Skip to content

Recover and automate mbeddr.cpp's test suite in Gradle/CI - #47

Open
mfsoliveira wants to merge 23 commits into
DSLFoundry:mainfrom
mfsoliveira:main
Open

Recover and automate mbeddr.cpp's test suite in Gradle/CI#47
mfsoliveira wants to merge 23 commits into
DSLFoundry:mainfrom
mfsoliveira:main

Conversation

@mfsoliveira

Copy link
Copy Markdown

The goal of this PR is to recover the available tests in mbeddr.cpp and make them run automatically when building it using gradle local or in a CI. In order for that to work, I also needed to update some workflows and build.gradle files

With that goal in mind, I avoided fixing issues found in the language itself, but worked around them. Therefore I followed the strategy:

  1. fix modeling issues in the test models or assertions;
  2. update tests using the current modeling constructs, because some of them were completely outdated;
  3. fix the language if it is a trivial fix;
  4. comment out the test content;
  5. delete test content completely.

Here is some notes about the issues I found and decided better not handling it: unhandled issues found

Concretely: of the 17 test.ex models, 14 now build, run, and pass end-to-end (33 test cases total, 0 failures). thisPointer no longer builds at all — its content is commented out pending a BuildConfiguration-generation-order bug. Static and specifier had their root nodes deleted, since they relied on removed language concepts; both are recoverable from history once those concepts are restored. Within the 14 passing modules, namespaces, nullPointers, and templates still have specific declarations or test cases commented out pending generator fixes (see unhandled issues found). Separately, the 190 test.ts (typesystem/language) tests continue to pass unaffected, aside from one operator-overload-matching that required correction.

Beyond recover the tests themselves, this PR makes mbeddr.cpp's Gradle build and its three GitHub Actions workflows (build/test, package publishing, and GitHub releases) reliably reproducible in any environment they run in — locally, in an fork's CI, or once merged upstream. That means a plain ./gradlew build behaves consistently whether or not clean ran first, every CI workflow that now depends on the test suite actually has the environment (a display, a C++ toolchain) that suite needs, and publishing and releasing work correctly from a fork's own repository and registry rather than assuming they always run in the upstream project. It also means CI failures are trustworthy: a build only reports success if the tests it ran actually passed, and repeated runs don't fail on artifact-immutability conflicts just because they happen to target the same version.

I tried to split changes into different commits, one commit per test model, with an explanation of the issues and fixes in each for issues and fixes on that. This PR is bigger than intended, because changes in the workflows were not expected. But I saw no value in recovering those tests if we cannot run them. Some of that value was already lost by commenting out or deleting test content. But we can retrieve them and implement fixes later.

That said, it is my first interaction with mbeddr.cpp. I am not familiar with this project yet, thus I avoided making too many language changes. If additional explanation or discussion is needed, feel free to contact me.

mfsoliveira and others added 23 commits August 24, 2026 15:59
…ed exactly, not just checked for being primitive

OverloadTypeChecker.matchesType treated any two primitive types as a
match (e.g. int16 matched an int8-only operator[] overload), which
silently hid type mismatches. Compare the actual concepts instead, and
update the OperatorOverloading test's stale expectations to match the
corrected behavior.
Static.mps and specifier.mps used removed language concepts (e.g.
StaticVar) and failed to build, blocking test.ex.com.mbeddr.cpp from
compiling at all. Delete their root nodes for now to unblock the rest
of the suite; recoverable from history once the underlying concepts
are ported or replaced.
… modules

Each module's BuildConfiguration was missing CppCoCompilationConfigItem,
so the generated Makefile only emitted .c.o compile rules and silently
skipped all .cpp/.cxx sources. Add it to configurationItems, and point
DesktopPlatform.cppCompiler at plain "g++" (resolved via PATH) with
-std=c++11 -fpermissive, matching com.mbeddr.cpp.__spreferences.PlatformTemplates'
own canonical DesktopPlatform template.
- dynamic_cast requires a polymorphic source type, but SomeClass had novirtual
  members, so test_dynamic_casting and test_reinterpret_casting failed to compile.
  Add a virtual destructor to BaseClass to make the hierarchy polymorphic

- test_dynamic_casting cast between ThirdClass and OtherClass, two
  unrelated sibling classes is not allowed. Fixed by exercising a downcast
  instead: a BaseClass* that actually points at a ThirdClass object,
  dynamic_cast back down to ThirdClass*, which correctly succeeds.

- test_reinterpret_casting cast the same ThirdClass/OtherClass pair via
  reinterpret_cast. It does a raw pointer reinterpretation with no runtime
  type checking, yelding a no-safe cast from a non-null input regardless of
  the actual types involved. Flipped to assert non-null, which is what reinterpret_cast
  actually and always guarantees here.
…nments

Move Subtractor from ImportedModule into Extension (ahead of Calculator)
so its base-class reference gets a proper #include; InheritanceInstance
doesn't extend TypeWithDeclaration, so mbeddr's cross-module dependency
scanner never emits one when the base class lives in another module.
This fix is a workaround so that no big changes are need in the language.
We need additional test to cover such ImportedModule case and fix new bug found.

Drop Counter.value2, an array field that hits an unrelated textgen bug
when declared this way. It is a workaround until an implementation
of textGen rules for ArrayAttributeInitExpression cover this language gap.

Fix extensionTC1: calculator.increment(someInt) and
calculator.increment20(someInt) discarded their return values instead
of assigning them back to someInt, and increment20 was called with a
stray literal (24) instead of someInt. This fix assumes the tester was
erroneously expecting changing values by references and so addjust the code.
…field init through ParentClass

Directly initializing an inherited field in a derived class's constructor
initializer list (parentY(3) on ClassName, where parentY belongs to
ParentClass) is invalid C++ — mbeddr.cpp let the model express it, but
generation had no way to emit it correctly. Restructure instead: give
ParentClass its own constructor (renaming the field to y) and have
ClassName's two-int constructor call it via ParentClass(inputY). This
also fixes a latent bug where inputY was accepted as a parameter but
never referenced anywhere, so it had no effect on the resulting object;
now it correctly determines y's value. Add a public getParentY() getter
and update the assertion to check the forwarded value (100) instead of
the old hardcoded literal.
… value

NewDeleteClass2::extendedInt was a raw pointer with no constructor and
no in-class initializer, so ndc2.extendedInt held garbage; dereferencing
it to assign 700 segfaulted. Point it at real memory via new before use,
matching the working pattern used a few lines above for ndc1Int. Also
fix the following assertion, which expected 600 despite 700 being what
was actually assigned.
…est pending generator fix

using D::dInt / using D::dBool / using namespace D::E, and the test
case that exercises them, always render before the namespaces they
reference regardless of where they sit in the model — mbeddr.cpp's
generator emits all global-using declarations as a group ahead of any
namespace declaration, which is invalid C++ when the referenced
namespace lives in the same file. Comment out the three
GlobalUsing* declarations, the namespaceGlobalUsing test case, and its
now-dangling TestCaseRef, so namespaceDeclaration and namespaceLocalUsing
can compile and pass. The generator ordering bug itself is unfixed and
tracked for later.
…ers test — missing #include <cstddef>

com.mbeddr.cpp.base has a correctly implemented mechanism for this:
Nullptr_tType.getRequiredImports() returns a StdHeaderImport for
<cstddef>, and CPPImplementationModule.importsForHeader() aggregates
it correctly. But importsForHeader() is never called from any
generator template in the repo, so the include never reaches the
generated header and every std::nullptr_t usage fails to compile.

Commented out the nullptr_t constructor, the null_ptr field, and the
testMethodNullPtr method on TestClass, the now-pointless
NullPointerTC1 test case (entirely about the null_ptr field), the
nullptr_t-constructor variables in NullPointerTC2, and the
testMethodNullPtr calls in NullPointerTC3. The int16*-pointer parts
of these test cases are untouched and still pass.

TODO: wire importsForHeader() into the actual header-generation
template so std::nullptr_t (and the other computed std headers it
already handles) works, then uncomment the above.
…ve SomeClass real arithmetic

com.mbeddr.cpp.modules.gen's operator-prototype header templates
(Binary/PrePostfix/ArrayAccess) hardcoded the return type as void with
no macro to substitute the real one, so every overloaded operator's
header declaration said void regardless of what it actually returned,
while the .cpp definition (generated through a different path) used
the real return type. Any operator with a non-void return type failed
to compile. Added the missing CopySrcNodeMacro to copy the real
declared return type into the header, matching the existing
operator-symbol and argument-list macros already in those templates.

Also gave SomeClass real arithmetic based on its _x field (+, %, +=,
[], ++, ==) instead of placeholder bodies that echoed arguments back
or returned a hardcoded true, and updated test_operators' asserts to
check real computed values instead of comparing incompatible types
or values that were only ever trivially true.
…nimal passing test

Three generator bugs make most of this test module's content impossible
to compile as-is: free function templates lose their `template<class T>`
on forward-declared prototypes; template classes with explicit
constructors get spurious, wrongly-qualified duplicate out-of-line
definitions in the .cpp (constructor-free template classes are
unaffected); and List/IntList have an emission-order issue. None of
these have a simple model-level workaround, so they need an actual
generator fix later.

For now, content that exercises them is commented out, leaving one real
passing test so the module builds and asserts something instead of
failing outright or being replaced with a fake bypass.

- Commented out identity/compare/deref/multiplyBy (free template
  functions) and their tests, plus List, IntList, sum, and classTest --
  all blocked by the generator bugs above.
- Added trivialTemplateTest, using a constructor-free Box<T>, as the
  one active, genuinely passing test.
- Fixed IntList's constructor: it directly initialized `head`, a field
  inherited from List<T> (invalid C++), and passed a nonsensical second
  argument referencing `tail` before List's part of the object was
  constructed. Now delegates properly to List's two-argument
  constructor with NULL.
- Gave List<T> a default constructor -- two call sites relied on
  default construction with none defined.
- Reorganized the model so templates are declared near the tests that
  use them, and removed unused constructs.
Person/List<T>/thisTC1/main in thisPointer.mps commented out and its
BuildConfiguration removed — classes in that model never got proper
header/visibility-section generation. It persisted after trying different
experiments. For some reason only pre-existing build configurations
generate class visibility sections correctly.
Fixes:
- Add missing default constructor to List<T> in thisPointer test
- I commented the content so that we can move on and fix it later.
…tore SimpleCounter and namespace tests

simple_classes.mps declared the same model UUID as classes.mps since its
first commit into this repo, so MPS silently dropped it on every load and
its tests have effectively never compiled or run. This fix:

- Give the model its own UUID so it's no longer discarded as a duplicate;
- Re-implement im1cpp with public methods, so that it can be accessed in the test cases;
- Reimplement nsincpp from scratch on current mbeddr.cpp concepts;
`./gradlew build` generates and runs both test suites (test.ts, test.ex),
fails on real test/build failures, and merges both suites' results into
one JUnit/HTML report. CI builds and tests on every push, runs with
--continue so one suite's failure doesn't prevent the other from being
reported, and uploads all reports as an artifact.

Dependency versions are pinned for reproducible builds, with
repositories centralized in settings.gradle. testExTests compiles and
runs test.ex modules concurrently, classifying library vs. test
directories by path segments relative to sourceGenDir so it doesn't
depend on where the repo is checked out. Each test run clears stale
TEST-*.xml first, and the unified report glob is recursive so
results can't be silently dropped.

Notable decisions:
- The "com.jetbrains:mps" Maven artifact is missing lib/jna.jar (real
  JNA classes + native libs) that a full MPS install has; without it
  the test JVM crashes on native UI classloading outside a real MPS
  app. Patched a real JNA jar onto the generated <junit> task's
  classpath, plus -Djava.awt.headless=true and
  -Didea.filewatcher.disabled=true jvmargs.
  (It's macOS-specific.)
Added in a single 2021 migration commit alongside real edits to tests/
itself, and never touched again since -- a forgotten local backup of
tests/ from just before that migration, not a parallel copy with any
value of its own.
…on in check_GlobalVarDecCPP

The rule validates char16_t/char32_t/wchar_t init ranges by parsing the
rendered initializer as an int, which breaks for char literals like
'1' (renders with quotes). Now handles CharLiteral initializers
directly instead of parsing their text.
Sure I also cleaned up depenencies as I touched check_GlobalVarDecCPP's module.
…le publishing

publish now depends on build (added when check.dependsOn testLanguages was
wired in), so `./gradlew publish` runs the full test suite, including
testTsTests, which boots a real (headed) MPS/IntelliJ-platform instance.
publish-maven.yml never installed a C++ toolchain or a display for it,
unlike the "Java CI with Gradle" workflow, so the JUnit suite errored at
setup with no display to attach to. Add the same g++/make/xvfb install
step and wrap the publish command in xvfb-run, matching gradle.yml.

The GitHub Packages URL was hardcoded to DSLFoundry/mbeddr.cpp, so a push
to a fork's main would authenticate correctly (via that fork's own
GITHUB_TOKEN) but try to publish into the upstream org's package registry,
where a fork's token has no write access. Resolve the URL from
GITHUB_REPOSITORY instead -- set automatically by GitHub Actions to
whichever repo the workflow is actually running in -- so the same
workflow file publishes to a fork's own registry when run there, and to
upstream's once merged, with no per-repo configuration needed. Falls back
to DSLFoundry/mbeddr.cpp for a plain local `./gradlew publish` run outside
CI.

Also bump actions/checkout and actions/setup-java to v5 (both flagged
deprecated) and declare explicit `permissions: packages: write` rather
than relying on each repo's default token permissions.
…le releasing

githubRelease depends on build (added when check.dependsOn testLanguages
was wired in), so `./gradlew githubRelease` runs the full test suite,
including testTsTests, which boots a real (headed) MPS/IntelliJ-platform
instance. release.yml never installed a C++ toolchain or a display for
it -- the same gap publish-maven.yml had -- so the JUnit suite errored at
setup with no display to attach to. Add the same g++/make/xvfb install
step and wrap the release command in xvfb-run, matching the other two
workflows.

githubRelease's owner/repo were hardcoded to DSLFoundry/mbeddr.cpp, so a
release triggered by a push to a fork's main would try to create the
release under the upstream org, where a fork's token has no permission.
Resolve both from GITHUB_REPOSITORY instead, shared with the publishing{}
block's own URL resolution via one ghRepository variable, so the same
config publishes/releases against whichever repo it's actually running
in, fork or upstream, with no per-repo changes needed.

release.yml passed CI_COMMIT_SHA via the `env` expression context
(${{ env.GITHUB_SHA }}), which only exposes variables explicitly declared
in an env: block -- the commit SHA lives in the `github` context instead,
so this always evaluated to empty. Use ${{ github.sha }} directly. Also
update targetCommitish's local fallback from "master" to "main" to match
this repo's actual default branch.

Also bump actions/checkout (v2.3.4) and actions/setup-java (v1) to v5,
both long past end of support, and declare explicit `permissions:
contents: write` -- githubRelease creates a release and uploads assets,
so this states what it actually needs rather than relying on the repo's
default token permissions.
…adless-tests

Fix release workflow: support headless test execution and fork-portable releasing
publish and githubRelease both currently run on every push to main --
there's no separate release-branch or tag process yet -- so
mbeddrCppVersion alone isn't unique per run. GitHub Packages rejects
republishing an existing version (409 Conflict), and GitHub Releases
rejects recreating an existing tag/release, so the second push after any
successful publish/release always fails.

Add publishVersion: mbeddrCppVersion with the CI run number and short
commit SHA appended (GitHub Actions' equivalent of a build counter plus
git SHA -- the same scheme iets3.core uses for its own CI-triggered
builds). A plain local build, with no GITHUB_RUN_NUMBER set, keeps the
bare base version unchanged. Use publishVersion for the Maven publication
version and the GitHub Release's tag/release name; mbeddrCppVersion
itself is untouched and still names the actual build artifact
(releaseArtifacts' file glob depends on it matching the real, already-built
zip filename).
Give published/released versions a unique identifier per CI run
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant