Skip to content

Use SLF4J for runtime diagnostics - #15993

Open
jamesfredley wants to merge 6 commits into
8.0.xfrom
refactor/runtime-logging-slf4j
Open

Use SLF4J for runtime diagnostics#15993
jamesfredley wants to merge 6 commits into
8.0.xfrom
refactor/runtime-logging-slf4j

Conversation

@jamesfredley

Copy link
Copy Markdown
Contributor

Summary

  • replace direct stack traces and console output with structured SLF4J diagnostics across selected runtime paths
  • preserve stderr visibility when logging is disabled or no provider is installed
  • preserve historical logger categories and command return contracts
  • add focused provider-enabled, API-only, lifecycle, malformed-input, and schema-export regression coverage

Verification

  • :grails-bootstrap:test
  • :grails-core:test
  • :grails-shell-cli:test
  • :grails-data-hibernate5:test
  • :grails-data-hibernate7:test
  • grails-forge :grails-forge-core:test
  • git diff --check

This is an AI-generated starting point for the broader runtime logging cleanup.

Assisted-by: opencode:gpt-5.6-sol

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR replaces direct printStackTrace() / System.out.println() runtime diagnostics with SLF4J logging across several Grails runtime paths (CLI startup, Spring application runner lifecycle, plugin manager profiling, schema export, and Forge POM parsing), while adding regression tests that validate behavior both with and without an SLF4J provider on the classpath.

Changes:

  • Switched multiple runtime error/reporting paths from raw stack traces / console output to structured SLF4J logging with fallback-to-stderr behavior where appropriate.
  • Added focused, forked-process regression specs to verify provider-enabled vs API-only (no provider) behavior and to prevent stderr/log duplication.
  • Updated test dependencies to support provider-backed assertions (Logback appenders) and provider/no-provider classpath variants.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
grails-shell-cli/src/test/resources/org/grails/cli/compiler/dependencies/spring-boot-dependencies-effective-bom.xml Adds a minimal effective BOM test resource fixture.
grails-shell-cli/src/test/groovy/org/grails/cli/GrailsCliSpec.groovy New forked-process spec verifying shared settings parse failures are logged and surfaced on stderr.
grails-shell-cli/src/test/groovy/org/grails/cli/GrailsCliLoggingVerifier.groovy Logback-based verifier that asserts a single ERROR event and stderr prefix behavior.
grails-shell-cli/src/test/groovy/org/grails/cli/command/run/SpringApplicationRunnerSpec.groovy New forked-process lifecycle spec verifying failure reporting with logging on/off.
grails-shell-cli/src/test/groovy/org/grails/cli/command/run/LifecycleVerifier.groovy Logback-based verifier for runner lifecycle errors and stderr duplication rules.
grails-shell-cli/src/main/groovy/org/grails/cli/GrailsCli.groovy Replaces settings load printStackTrace() with LOG.error(..., e) while preserving stderr summary.
grails-shell-cli/src/main/groovy/org/grails/cli/command/run/SpringApplicationRunner.java Replaces stack traces with LOG.error/warn when enabled, otherwise prints stack traces to stderr.
grails-shell-cli/build.gradle Adds Logback for tests (and adjusts logging-related dependencies).
grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/build/dependencies/PomDependencyVersionResolverSpec.groovy Adds malformed-POM test ensuring parsing failures are WARN-logged and skipped.
grails-forge/grails-forge-core/src/main/java/org/grails/forge/build/dependencies/PomDependencyVersionResolver.java Replaces printStackTrace() with LOG.warn(...) on malformed POM parsing.
grails-forge/grails-forge-core/build.gradle Moves Logback dependency to testImplementation to support logback-based tests.
grails-data-hibernate7/grails-plugin/src/test/groovy/grails/plugin/hibernate/commands/SchemaExportCommandSpec.groovy New forked-process spec verifying schema export failures are visible with/without a provider.
grails-data-hibernate7/grails-plugin/src/main/groovy/grails/plugin/hibernate/commands/SchemaExportCommand.groovy Uses SLF4J for schema export failures when enabled; falls back to stderr stack traces otherwise.
grails-data-hibernate7/grails-plugin/build.gradle Adds slf4j-simple test runtime for provider-enabled scenario.
grails-data-hibernate5/grails-plugin/src/test/groovy/grails/plugin/hibernate/commands/SchemaExportCommandSpec.groovy New forked-process spec mirroring Hibernate 7 coverage.
grails-data-hibernate5/grails-plugin/src/main/groovy/grails/plugin/hibernate/commands/SchemaExportCommand.groovy Same SLF4J + stderr fallback behavior for Hibernate 5 schema export.
grails-data-hibernate5/grails-plugin/build.gradle Adds slf4j-simple test runtime for provider-enabled scenario.
grails-core/src/test/groovy/org/grails/plugins/ProfilingGrailsPluginManagerSpec.groovy New spec asserting profiling output is emitted via SLF4J (stderr), not stdout.
grails-core/src/main/groovy/org/grails/plugins/ProfilingGrailsPluginManager.java Replaces stdout profiling messages with SLF4J info while preserving logger category.
grails-bootstrap/src/test/groovy/grails/build/logging/GrailsConsoleLoggingSpec.groovy New forked-process spec verifying invalid console configuration is visible with/without provider.
grails-bootstrap/src/main/groovy/grails/build/logging/GrailsConsole.java Logs console instantiation failures via SLF4J when enabled; otherwise prints stack trace.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 116 to 119
// Testing
testImplementation 'org.slf4j:slf4j-simple'
testImplementation 'ch.qos.logback:logback-classic'
testImplementation 'org.spockframework:spock-core'
@bito-code-review

Copy link
Copy Markdown

The concern regarding multiple SLF4J providers on the test classpath is valid, as SLF4J 2.x can exhibit non-deterministic behavior in such environments. The current PR introduces a new test, GrailsConsoleLoggingSpec, which explicitly manages the test runtime classpath by filtering out slf4j-simple and logback-classic when apiOnly is true. This approach effectively isolates the test environment to verify logging behavior without ambiguous bindings, aligning with the goal of having a controlled provider environment during testing.

grails-bootstrap/src/test/groovy/grails/build/logging/GrailsConsoleLoggingSpec.groovy

private static String testRuntimeClasspath(boolean apiOnly) {
        String[] entries = System.getProperty('java.class.path').split(File.pathSeparator)
        if (!apiOnly) {
            return entries.join(File.pathSeparator)
        }
        entries.findAll { !it.contains('slf4j-simple') && !it.contains('logback-classic') }.join(File.pathSeparator)
    }

@codecov

codecov Bot commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 13.88889% with 31 lines in your changes missing coverage. Please review.
✅ Project coverage is 51.5073%. Comparing base (3c5b1b9) to head (936ad90).

Files with missing lines Patch % Lines
...g/grails/plugins/ProfilingGrailsPluginManager.java 17.6471% 14 Missing ⚠️
...rails/cli/command/run/SpringApplicationRunner.java 0.0000% 13 Missing ⚠️
...ain/groovy/grails/build/logging/GrailsConsole.java 25.0000% 3 Missing ⚠️
...li/src/main/groovy/org/grails/cli/GrailsCli.groovy 50.0000% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                 @@
##                8.0.x     #15993        +/-   ##
==================================================
+ Coverage     51.4814%   51.5073%   +0.0259%     
- Complexity      17775      17786        +11     
==================================================
  Files            2039       2039                
  Lines           95586      95600        +14     
  Branches        16591      16594         +3     
==================================================
+ Hits            49209      49241        +32     
+ Misses          39066      39047        -19     
- Partials         7311       7312         +1     
Files with missing lines Coverage Δ
...li/src/main/groovy/org/grails/cli/GrailsCli.groovy 1.8868% <50.0000%> (+0.3095%) ⬆️
...ain/groovy/grails/build/logging/GrailsConsole.java 37.4101% <25.0000%> (-0.0295%) ⬇️
...rails/cli/command/run/SpringApplicationRunner.java 0.0000% <0.0000%> (ø)
...g/grails/plugins/ProfilingGrailsPluginManager.java 23.4568% <17.6471%> (+23.4568%) ⬆️

... and 5 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jdaugherty

Copy link
Copy Markdown
Contributor

It's good to see this can work - I remember reading comments in the code base that historically this couldn't work due to some edge case. I'll see if I can dig up the history to make sure this is indeed safe now and then review.

@jamesfredley jamesfredley moved this to Todo in Apache Grails Jul 24, 2026
borinquenkid and others added 3 commits July 27, 2026 13:48
Copilot flagged that grails-shell-cli's test classpath carries both
slf4j-simple and logback-classic simultaneously, which SLF4J 2.x treats
as an ambiguous binding. Verified this is real: `dependencies
--configuration testRuntimeClasspath` resolves both providers together,
and neither can simply be dropped - slf4j-simple is needed at runtime
for the actual `grails` CLI executable's production logging, and
logback-classic is needed by the new Logback-appender-based forked
regression specs (SpringApplicationRunnerSpec, GrailsCliSpec).

Confirmed no current regression from this (full :grails-shell-cli:test
run is green, no "multiple SLF4J providers" warning appears in any test
output) because both new specs that need a specific provider already
pin it explicitly via -Dslf4j.provider=... in their forked child
process. This commit removes the now-fully-redundant duplicate
`testImplementation 'org.slf4j:slf4j-simple'` line (already pulled in
via the runtimeOnly dependency, so this doesn't change the resolved
classpath) and documents the coexistence so a future forked-process
fixture doesn't skip the explicit pin and hit non-deterministic
provider selection.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…age move

The 8.0.x branch relocated ApplicationCommand/ExecutionContext and each
SchemaExportCommand into a dedicated cli source set with new packages
(grails.dev.commands.ExecutionContext -> org.apache.grails.core.cli.
ExecutionContext, grails.plugin.hibernate.commands.SchemaExportCommand
-> org.apache.grails.data.hibernate{5,7}.cli.SchemaExportCommand) as
part of unrelated work that landed after this PR's base commit.

Merging origin/8.0.x picked this up cleanly via rename tracking for the
production SchemaExportCommand.groovy files (already updated to the new
package on merge), but the two new SchemaExportCommandSpec.groovy test
files added by this PR had no prior history for git to track the rename
through, so they were merged in unchanged - still importing the old
ExecutionContext package and referencing SchemaExportCommand unqualified
(previously fine only because the spec shared its production class's old
package). Both left compileTestGroovy failing after the merge. Fixed by
importing both classes from their new locations.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@borinquenkid

Copy link
Copy Markdown
Member

@jdaugherty — some findings that might help with your investigation into whether this can safely work.

I searched git history broadly for the historical comment/reason you're remembering (grepped commit messages and blamed the touched files - GrailsConsole.java, SpringApplicationRunner.java, GrailsCli.groovy) and couldn't turn up a specific citation. If you find it, I'd like to see it too.

What I did instead: identified the most plausible place the "edge case" could bite, and tested it directly. SpringApplicationRunner.RunThread deliberately runs with the compiled application's own classloader as its context classloader (its own Javadoc: "Thread used to launch the Spring Application with the correct context classloader"). SLF4J's provider discovery is classloader-sensitive, so a logger call whose first-ever SLF4J touch in the JVM happens from inside that thread is exactly the kind of scenario that could pick an ambiguous/wrong provider.

That doesn't happen here, structurally: LOG on SpringApplicationRunner is a static final field, so SLF4J's one-time provider binding is triggered when the class is first initialized - which happens when new SpringApplicationRunner(...) is constructed on the main CLI thread, before any RunThread with a foreign classloader is ever spawned. By the time RunThread.run() calls logOrPrintStackTrace, the logger is already resolved.

I verified this empirically rather than just reasoning about it: ran SpringApplicationRunnerSpec (6/6 pass), which forks a real JVM and exercises exactly this path - launch/reload/shutdown failures thrown from inside RunThread and logged via SLF4J. Also ran the full stated verification suite (grails-bootstrap, grails-core, grails-shell-cli, grails-data-hibernate5, grails-data-hibernate7, grails-forge-core) clean on a fresh --rerun-tasks run.

Separately, I confirmed and fixed the multi-SLF4J-provider classpath issue Copilot flagged (grails-shell-cli's test classpath had both slf4j-simple and logback-classic on it simultaneously with no exclusivity) and rebased onto current 8.0.x, which also surfaced a real semantic merge conflict: ExecutionContext/SchemaExportCommand moved packages upstream as part of unrelated work, and the two new SchemaExportCommandSpec files (added by this PR, so git had no history to rename-track through) needed their imports updated - now fixed.

This is evidence for your review, not a claim that the question is settled - happy to dig further if you want a different angle tested.

@borinquenkid borinquenkid added this to the grails:8.0.0-RC1 milestone Jul 27, 2026
Codecov reported 13.88% patch coverage because the fixture specs verify
the new logging behavior in forked JVMs that JaCoCo never instruments.
Forward the test JVM's JaCoCo -javaagent argument into the forked
processes so their execution data appends to the module exec file, and
exercise the remaining ProfilingGrailsPluginManager configuration
phases in-process with a probe plugin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@testlens-app

testlens-app Bot commented Aug 2, 2026

Copy link
Copy Markdown

🚨 TestLens detected 18 failed tests 🚨

Here is what you can do:

  1. Inspect the test failures carefully.
  2. If you are convinced that some of the tests are flaky, you can mute them below.
  3. Finally, trigger a rerun by checking the rerun checkbox.

Test Summary

CI - Groovy Joint Validation Build / build_grails > :grails-core:test

Test Runs Flakiness
ProfilingGrailsPluginManagerSpec > configuration phases emit INFO profiling messages for each plugin 0% 🟢
ProfilingGrailsPluginManagerSpec > deprecated constructors log their warning in the historical category 0% 🟢
ProfilingGrailsPluginManagerSpec > plugin loading emits INFO profiling messages instead of standard output 0% 🟢

CI / Build Grails-Core (macos-latest, 21) > :grails-core:test

Test Runs Flakiness
ProfilingGrailsPluginManagerSpec > configuration phases emit INFO profiling messages for each plugin 0% 🟢
ProfilingGrailsPluginManagerSpec > deprecated constructors log their warning in the historical category 0% 🟢
ProfilingGrailsPluginManagerSpec > plugin loading emits INFO profiling messages instead of standard output 0% 🟢

CI / Build Grails-Core (ubuntu-latest, 21) > :grails-core:test

Test Runs Flakiness
ProfilingGrailsPluginManagerSpec > configuration phases emit INFO profiling messages for each plugin 0% 🟢
ProfilingGrailsPluginManagerSpec > deprecated constructors log their warning in the historical category 0% 🟢
ProfilingGrailsPluginManagerSpec > plugin loading emits INFO profiling messages instead of standard output 0% 🟢

CI / Build Grails-Core (ubuntu-latest, 25) > :grails-core:test

Test Runs Flakiness
ProfilingGrailsPluginManagerSpec > configuration phases emit INFO profiling messages for each plugin 0% 🟢
ProfilingGrailsPluginManagerSpec > deprecated constructors log their warning in the historical category 0% 🟢
ProfilingGrailsPluginManagerSpec > plugin loading emits INFO profiling messages instead of standard output 0% 🟢

CI / Build Grails-Core (windows-latest, 25) > :grails-core:test

Test Runs Flakiness
ProfilingGrailsPluginManagerSpec > configuration phases emit INFO profiling messages for each plugin 0% 🟢
ProfilingGrailsPluginManagerSpec > deprecated constructors log their warning in the historical category 0% 🟢
ProfilingGrailsPluginManagerSpec > plugin loading emits INFO profiling messages instead of standard output 0% 🟢

CI / Build Grails-Core Rerunning all Tasks (ubuntu-latest, 21) > :grails-core:test

Test Runs Flakiness
ProfilingGrailsPluginManagerSpec > configuration phases emit INFO profiling messages for each plugin 0% 🟢
ProfilingGrailsPluginManagerSpec > deprecated constructors log their warning in the historical category 0% 🟢
ProfilingGrailsPluginManagerSpec > plugin loading emits INFO profiling messages instead of standard output 0% 🟢

🏷️ Commit: c75b73e
▶️ Tests: 57090 executed
⚪️ Checks: 60/60 completed

Test Failures (first 10 of 18)

ProfilingGrailsPluginManagerSpec > configuration phases emit INFO profiling messages for each plugin (:grails-core:test in CI / Build Grails-Core (macos-latest, 21))
Condition not satisfied:

[ 'doArtefactConfiguration started', 'doWithSpring started', 'doWithSpring for plugin [profilingProbe] started', 'doWithDynamicMethods started', 'doWithDynamicMethods for plugin [profilingProbe] started', 'doWithApplicationContext started', 'doWithApplicationContext for plugin [profilingProbe] started' ].every { message -> output.contains("INFO grails.plugins.DefaultGrailsPluginManager - $message") }
                                                                                                                                                                                                                                                                                                                  |
                                                                                                                                                                                                                                                                                                                  false

	at org.grails.plugins.ProfilingGrailsPluginManagerSpec.configuration phases emit INFO profiling messages for each plugin(ProfilingGrailsPluginManagerSpec.groovy:101)
ProfilingGrailsPluginManagerSpec > deprecated constructors log their warning in the historical category (:grails-core:test in CI / Build Grails-Core (macos-latest, 21))
Condition not satisfied:

capturedErr.toString().contains('WARN grails.plugins.DefaultGrailsPluginManager - Using deprecated DefaultGrailsPluginManager constructor.')
|           |          |
|           ""         false
java.io.ByteArrayOutputStream@243bc6df

	at org.grails.plugins.ProfilingGrailsPluginManagerSpec.deprecated constructors log their warning in the historical category(ProfilingGrailsPluginManagerSpec.groovy:135)
ProfilingGrailsPluginManagerSpec > plugin loading emits INFO profiling messages instead of standard output (:grails-core:test in CI / Build Grails-Core (macos-latest, 21))
Condition not satisfied:

capturedErr.toString().contains('INFO grails.plugins.DefaultGrailsPluginManager - Loading plugins started')
|           |          |
|           ""         false
java.io.ByteArrayOutputStream@315c868b

	at org.grails.plugins.ProfilingGrailsPluginManagerSpec.plugin loading emits INFO profiling messages instead of standard output(ProfilingGrailsPluginManagerSpec.groovy:51)
ProfilingGrailsPluginManagerSpec > configuration phases emit INFO profiling messages for each plugin (:grails-core:test in CI / Build Grails-Core (ubuntu-latest, 21))
Condition not satisfied:

[ 'doArtefactConfiguration started', 'doWithSpring started', 'doWithSpring for plugin [profilingProbe] started', 'doWithDynamicMethods started', 'doWithDynamicMethods for plugin [profilingProbe] started', 'doWithApplicationContext started', 'doWithApplicationContext for plugin [profilingProbe] started' ].every { message -> output.contains("INFO grails.plugins.DefaultGrailsPluginManager - $message") }
                                                                                                                                                                                                                                                                                                                  |
                                                                                                                                                                                                                                                                                                                  false

	at org.grails.plugins.ProfilingGrailsPluginManagerSpec.configuration phases emit INFO profiling messages for each plugin(ProfilingGrailsPluginManagerSpec.groovy:101)
ProfilingGrailsPluginManagerSpec > deprecated constructors log their warning in the historical category (:grails-core:test in CI / Build Grails-Core (ubuntu-latest, 21))
Condition not satisfied:

capturedErr.toString().contains('WARN grails.plugins.DefaultGrailsPluginManager - Using deprecated DefaultGrailsPluginManager constructor.')
|           |          |
|           ""         false
java.io.ByteArrayOutputStream@314e6795

	at org.grails.plugins.ProfilingGrailsPluginManagerSpec.deprecated constructors log their warning in the historical category(ProfilingGrailsPluginManagerSpec.groovy:135)
ProfilingGrailsPluginManagerSpec > plugin loading emits INFO profiling messages instead of standard output (:grails-core:test in CI / Build Grails-Core (ubuntu-latest, 21))
Condition not satisfied:

capturedErr.toString().contains('INFO grails.plugins.DefaultGrailsPluginManager - Loading plugins started')
|           |          |
|           ""         false
java.io.ByteArrayOutputStream@c119654

	at org.grails.plugins.ProfilingGrailsPluginManagerSpec.plugin loading emits INFO profiling messages instead of standard output(ProfilingGrailsPluginManagerSpec.groovy:51)
ProfilingGrailsPluginManagerSpec > configuration phases emit INFO profiling messages for each plugin (:grails-core:test in CI / Build Grails-Core (ubuntu-latest, 25))
Condition not satisfied:

[ 'doArtefactConfiguration started', 'doWithSpring started', 'doWithSpring for plugin [profilingProbe] started', 'doWithDynamicMethods started', 'doWithDynamicMethods for plugin [profilingProbe] started', 'doWithApplicationContext started', 'doWithApplicationContext for plugin [profilingProbe] started' ].every { message -> output.contains("INFO grails.plugins.DefaultGrailsPluginManager - $message") }
                                                                                                                                                                                                                                                                                                                  |
                                                                                                                                                                                                                                                                                                                  false

	at org.grails.plugins.ProfilingGrailsPluginManagerSpec.configuration phases emit INFO profiling messages for each plugin(ProfilingGrailsPluginManagerSpec.groovy:101)
ProfilingGrailsPluginManagerSpec > deprecated constructors log their warning in the historical category (:grails-core:test in CI / Build Grails-Core (ubuntu-latest, 25))
Condition not satisfied:

capturedErr.toString().contains('WARN grails.plugins.DefaultGrailsPluginManager - Using deprecated DefaultGrailsPluginManager constructor.')
|           |          |
|           ""         false
java.io.ByteArrayOutputStream@3dc421ae

	at org.grails.plugins.ProfilingGrailsPluginManagerSpec.deprecated constructors log their warning in the historical category(ProfilingGrailsPluginManagerSpec.groovy:135)
ProfilingGrailsPluginManagerSpec > plugin loading emits INFO profiling messages instead of standard output (:grails-core:test in CI / Build Grails-Core (ubuntu-latest, 25))
Condition not satisfied:

capturedErr.toString().contains('INFO grails.plugins.DefaultGrailsPluginManager - Loading plugins started')
|           |          |
|           ""         false
java.io.ByteArrayOutputStream@2d320a24

	at org.grails.plugins.ProfilingGrailsPluginManagerSpec.plugin loading emits INFO profiling messages instead of standard output(ProfilingGrailsPluginManagerSpec.groovy:51)
ProfilingGrailsPluginManagerSpec > configuration phases emit INFO profiling messages for each plugin (:grails-core:test in CI / Build Grails-Core (windows-latest, 25))
Condition not satisfied:

[ 'doArtefactConfiguration started', 'doWithSpring started', 'doWithSpring for plugin [profilingProbe] started', 'doWithDynamicMethods started', 'doWithDynamicMethods for plugin [profilingProbe] started', 'doWithApplicationContext started', 'doWithApplicationContext for plugin [profilingProbe] started' ].every { message -> output.contains("INFO grails.plugins.DefaultGrailsPluginManager - $message") }
                                                                                                                                                                                                                                                                                                                  |
                                                                                                                                                                                                                                                                                                                  false

	at org.grails.plugins.ProfilingGrailsPluginManagerSpec.configuration phases emit INFO profiling messages for each plugin(ProfilingGrailsPluginManagerSpec.groovy:101)

Muted Tests

Select tests to mute in this pull request:

  • ProfilingGrailsPluginManagerSpec > configuration phases emit INFO profiling messages for each plugin
  • ProfilingGrailsPluginManagerSpec > deprecated constructors log their warning in the historical category
  • ProfilingGrailsPluginManagerSpec > plugin loading emits INFO profiling messages instead of standard output

Reuse successful test results:

  • ♻️ Only rerun the tests that failed or were muted before

Click the checkbox to trigger a rerun:

  • Rerun jobs

Learn more about TestLens at testlens.app.

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

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

4 participants