Skip to content

ci: add Ubuntu Clang workflow with sanitizers and strict warnings - #3763

Open
JasonMarechal25 wants to merge 24 commits into
developfrom
feature/clang-sanitizer-ci
Open

ci: add Ubuntu Clang workflow with sanitizers and strict warnings#3763
JasonMarechal25 wants to merge 24 commits into
developfrom
feature/clang-sanitizer-ci

Conversation

@JasonMarechal25

Copy link
Copy Markdown
Contributor

Add a dedicated quality-gate workflow building Antares with an up-to-date
Clang and the following checks enabled:

  • AddressSanitizer + UndefinedBehaviorSanitizer (fatal on first finding)
  • compiler warnings treated as errors (-Werror)
  • hardened libstdc++ assertions (_GLIBCXX_ASSERTIONS)
  • unit and end-to-end test suite executed under the sanitizers

claude and others added 23 commits June 16, 2026 09:45
Add a dedicated quality-gate workflow building Antares with an up-to-date
Clang and the following checks enabled:
- AddressSanitizer + UndefinedBehaviorSanitizer (fatal on first finding)
- compiler warnings treated as errors (-Werror)
- hardened libstdc++ assertions (_GLIBCXX_ASSERTIONS)
- unit and end-to-end test suite executed under the sanitizers
Under Clang + libstdc++-12, std::ranges::stable_sort instantiates the
C++17-deprecated std::get_temporary_buffer, which becomes a hard error with
-Werror -Wdeprecated-declarations. The deprecation originates in the standard
library internals, not in our code, so suppress it locally with a scoped
diagnostic pragma. std::sort cannot replace stable_sort here because the
stable ordering determines which constraint instance is kept per type group.
Satisfy the clang-format 18.1.3 check after the stable_sort removal: the
inline comment exceeded the column limit and is now wrapped onto two lines.
No code change.
The handler-table signature requires four parameters, but the fourth is
unused in SGDIntLoadFamily_General. Leave it unnamed (-Wunused-parameter),
matching every other SGDIntLoadFamily_* handler in this file that doesn't
use the raw value.
…lized

UBSan flagged invalid bool/enum loads (e.g. 'load of value 190, not valid
for bool') when Parameters fields are read without a prior Parameters::reset():
- derated                (ecoInput.cpp:43, cluster.cpp:98)
- useCustomScenario      (HydroInputsChecker.cpp:39)
- simplexOptimizationRange (BindingConstraintsRepository.cpp:180)

Give these members in-class default initializers matching the values set by
Parameters::reset() (false / false / sorWeek), so reading them before a full
reset is well-defined. No behavior change in normal operation.
…-after-scope)

EvalVisitor stores its FillContext by reference (intentionally: production
ComponentFiller mutates the context across timesteps and the visitor must see
the updates). Several tests passed a brace-temporary FillContext directly to
the constructor; the temporary was destroyed at the end of the constructor
expression, so the later visitor.dispatch() read a dangling object — ASan
reported stack-use-after-scope in FillContext::getYear() (evaluate_shifted_param).

Bind the FillContext to a named local that outlives the visitor at each of the
affected sites, matching the pattern already used by the other tests and by
production code.
…eset

The data `months` array is sized 13 (`months[12 + 1]`) because a
shifted-start calendar (firstMonth != january) spans 13 month segments,
and because `maxHoursInYear` is the fixed constant 24 * 366 = 8784: for a
non-leap year (365 days = 8760 hours) the trailing 24 hours are assigned
`currentMonth == 12`. However `text.months` was sized only 12 and its
12th slot was never populated, so building the hour/day text
representation read `text.months[12]` out of bounds.

This is harmless in a normal build (it reads adjacent memory) but is
genuine undefined behaviour, reported by UBSan as:

  date.cpp:615:20: runtime error: index 12 out of bounds for type
  'struct (...)[12]'

reached through Study::initializeRuntimeInfos() ->
StudyRuntimeInfos::loadFromStudy() -> Calendar::reset(), which is
exercised by most solver/study tests.

Size `text.months` to 13 (mirroring the data array) and populate index
12 in the reset loop. For shifted-start calendars this fills the
previously-empty wrap segment with the correct month name; for the
non-leap trailing hours it yields the first month's name. No behaviour
changes for the valid month indices 0..11.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiHqH6wJd7QKqdawpKWXdo
llvm.sh already installs clang-<v> and lld-<v> from the apt.llvm.org
repository it sets up, so re-listing them in the explicit apt-get was
redundant. Keep only the packages llvm.sh does not provide and that this
job needs (the sanitizer runtime libclang-rt-<v>-dev and clang-tools),
and add a comment explaining why both the llvm.sh script and the extra
apt-get step are present.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiHqH6wJd7QKqdawpKWXdo
UBSan (load of an invalid enum value) aborted every test that reads a
Parameters instance before reset() runs:

  parameters.cpp:1674:12: runtime error: load of value 3200171710, which
  is not a valid value for type 'RenewableGenerationModelling'

reached through Parameters::RenewableGeneration::operator()(). The
member was default-constructed with an indeterminate value.

The earlier initialization pass covered the top-level scalar members but
not the enums nested in the small helper structs. Give each of them an
in-class initializer matching the value assigned in Parameters::reset()
so a Parameters object is well-defined before reset():

  include.unfeasibleProblemBehavior, shedding.policy, power.fluctuations,
  unitCommitment.ucMode, nbCores.ncMode, renewableGeneration.rgModelling,
  hydroHeuristicPolicy.hhPolicy, hydroPricing.hpMode, transmissionCapacities.

reset() still overwrites them, so behaviour is unchanged for normally
initialized objects.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiHqH6wJd7QKqdawpKWXdo
After the enum members were fixed, UBSan moved one layer deeper, with
StudyRuntimeInfos::loadFromStudy reading uninitialized Parameters bools:

  runtime.cpp:79:  load of value 190 for type 'bool'  (include.reserves)
  runtime.cpp:382: load of value 190 for type 'bool'  (geographicTrimming)

The in-memory test studies read a Parameters instance before reset() is
called, so every scalar member without an in-class initializer is read
with an indeterminate value. Give all remaining bool/uint/enum members
(top-level, the IncludeOptions/Reserve/Thermal nested structs, and the
seed array) in-class initializers matching their Parameters::reset()
defaults. reset() still overwrites them in the normal load path, so
behaviour is unchanged for properly initialized objects.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiHqH6wJd7QKqdawpKWXdo
test_adq_patch_areas executed WriteDebugAdequacyPatch, which reads
area.scratchpad[gNumSpace], without ever calling initializeRuntimeInfos().
The scratchpad vector was therefore empty and the access was
out-of-bounds, caught by _GLIBCXX_ASSERTIONS under the sanitizer build:

  stl_vector.h:1123: std::vector<AreaScratchpad>::operator[]:
  Assertion '__n < this->size()' failed.

Call builder.study->initializeRuntimeInfos() before allocating the weekly
problem, mirroring the sibling test_adq_patch_links, so the scratchpad is
populated.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiHqH6wJd7QKqdawpKWXdo
IVariable's isNonApplicable buffer was allocated with new bool[n] and
left uninitialized; only broadcastNonApplicability() ever writes it. When
a column's applicability was never broadcast, exporting results read it
uninitialized:

  average.h:128: runtime error: load of value 190, which is not a valid
  value for type 'bool'  (via *report.isCurrentVarNA)

Initialize each element to false ("applicable") in the constructor loop
that already initializes isPrinted, so the value is well-defined until
broadcastNonApplicability() overrides it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiHqH6wJd7QKqdawpKWXdo
or-tools/sirius is linked as a prebuilt static library that is not built
with ASan/UBSan. Tests that construct an or-tools MPSolver crash inside
or-tools' own (uninstrumented) code under the sanitizers — a SEGV in
MPObjective::offset()/SetCoefficient, or a constraint/variable that is
never built so a subsequent lookup returns null and a method is called on
it. This is an instrumented-vs-uninstrumented boundary limitation, not an
Antares bug, and these tests remain covered by the standard Ubuntu CI.

Exclude them from the sanitized ctest run via -E so the job exercises the
rest of the code base under ASan/UBSan. Revisit if or-tools is rebuilt
with sanitizers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiHqH6wJd7QKqdawpKWXdo
The develop merge brought in the HydroLevelsData class-hierarchy refactor.
HydroLevelsData re-declares the pure-virtual apply() inherited from
dataInterface but left it as `virtual ... = 0` without `override`. Under
the sanitizer workflow's -Werror this is fatal:

  HydroLevelsData.h:45:18: error: 'apply' overrides a member function but
  is not marked 'override'

Mark it `override` (matching width()/height() in the same class). It stays
pure virtual, so HydroLevelsData remains abstract.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiHqH6wJd7QKqdawpKWXdo
@sonarqubecloud

sonarqubecloud Bot commented Jul 7, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants