Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve testing with stats and errors per test section #498

Merged
merged 1 commit into from
Oct 24, 2024

Conversation

jjmaestro
Copy link
Contributor

Add some basic test stats to the test runner output for both SimpleReport and JUnitReport:

  • number of tests that pass/fail
  • % of success
  • number of assertions within each test that pass / fail

@bioball
Copy link
Contributor

bioball commented May 20, 2024

Thanks for the PR! FYI: I probably won't get around to reviewing this until after our 0.26 release is out in early June.

@jjmaestro
Copy link
Contributor Author

@bioball no worries! I actually submitted the PR too early :S I'm re-doing it all because (1) I didn't realize I had a bunch of tests failing and (2) the failures and other stuff made me re-think it all.

I'm currently thinking about how to best approach things... I might close the PR and re-open a new one once I have something that 100% works. E.g. I thought I was going to be able to change some of the XML format, thinking it was a bit freeform and I'm now learning about the different XML schemas for unit testing 😅 Still not sure which one Pkl is using and/or if I can change it to add more of the information I'd like to have, etc.

Anyway, TL;DR no rush :) Thanks!!

@jjmaestro jjmaestro changed the title Add basic test stats Improve testing with stats and errors per test section May 22, 2024
@jjmaestro
Copy link
Contributor Author

jjmaestro commented May 22, 2024

OK, I think I finally got it done :) I've refactored a bunch of stuff and fixed my original commit, here's the summary:

  • TestRunner: separated the running of each "section" of a test into its own try-catch so that we can evaluate each section separately even if another section failed (e.g. evaluate all examples even if one test in "facts" throws an exception). I've added tests that exercise this "new feature".

  • TestResults: refactored it a bit so that TestResults now has the three sections and the old TestResults becomes TestSectionResults. Also, TestSectionResults now has only one error, since that's what we get when there's an exception evaluating the Pkl code.

  • SimpleReport:

    • Separated reporting of each test section so that it's easy to see what facts or what examples fail.
    • Added a "stats line" to the module and section levels that reports how many tests / assertions pass/fail. Note that, when reporting errors, there's no stats for the section since the error is all the info we get. Similarly, when examples fail, there's no stats because each example is converted into its own test so there are no failed assertions to report, it's all-or-nothing.
    • For the examples converted to tests, when multiple examples share the same name I've also added a counter ("# 1" and so on) so that it's easier to identify which one is failing.
  • JUnitReport: fixed the reporting of failures as it wasn't consistent with "tests": the tests were the number of tests that we run but "failures" were the number of assertions that failed.

Here's some outputs that show these new features:

  • Some facts pass, some fail and some examples pass, with stats:
module test1 ❌ 66.7% pass [2 passed, 1 failed] (file:///private/tmp/wut/test1.pkl, line 1)
  facts ❌ 0.0% pass [0 passed, 1 failed]
    foo ❌ 50.0% pass [1 passed, 1 failed]
      10 == 11 ❌ (file:///private/tmp/wut/test1.pkl, line 6)
  examples ✅ 100.0% pass [2 passed]
    user 0✍️
    user 1✍️
  • Facts throws an error but now, we can still evaluate examples passing and failing:
module test ❌ 33.3% pass [1 passed, 2 failed] (file:///tmp/test.pkl, line 1)
  facts ❌
    Error:
    –– Pkl Error ––
    exception1
    
    9 | throw("exception1")
        ^^^^^^^^^^^^^^^^^^^
    at test#facts["error1"][#1] (file:///tmp/test.pkl, line 9)
    
    3 | facts {
        ^^^^^^^
    at test#facts (file:///tmp/test.pkl, line 3)
  examples ❌ 50.0% pass [1 passed, 1 failed]
    user 0 ✅
    user 1 #0 ❌
      (file:///tmp/test.pkl, line 25)
      Expected: (file:///tmp/test.pkl-expected.pcf, line 9)
      new {
        name = "Pigeon"
        age = 41
      }
      Actual: (file:///tmp/test.pkl-actual.pcf, line 9)
      new {
        name = "Pigeon"
        age = 40
      }

@jjmaestro
Copy link
Contributor Author

jjmaestro commented May 22, 2024

Ah, so I don't forget... I've been trying to find out if there's an XSD to validate the XML produced by JUnitReport and I eventually found Jenkins' xUnit plugin that lists a bunch of supported tools. In that list, I saw it mentions JUnit support for two schemas: Ant junit and Maven Surefire.

I checked both and Maven Surefire is the closest to the XML JUnitReport produces... however, it does not validate against the XSD (many elements are missing required attributes).

Is there a tool that loads the XML report that could serve as a validator of sorts? :-?

@bioball
Copy link
Contributor

bioball commented May 23, 2024

I've tried many times to look for a schema, but as far as I can tell, there's no real schema for JUnit reports. And, the many tools that create JUnit reports all differ in minor ways.

Tools like Jenkins that accept JUnit-style reports tend to just make a best-effort attempt to parse them, so, we just do the best we can to be conformant to what's out there.

@jjmaestro
Copy link
Contributor Author

Arrrg CI failed because of linting! I could have sworn I did a final gw spotlessApply before commiting :( Anyway, I've just re-committed it, hopefully it'll all be fine now!

@jjmaestro
Copy link
Contributor Author

OK, I've just pushed the resolved merge conflict... it passes all the same tests. @bioball is there any way to ensure the commit is not blocked in CI? I've seen it blocked ("on hold") for ~a week before :-/ Is there any reason why that happens? :-?

Thanks!!

@jjmaestro
Copy link
Contributor Author

jjmaestro commented May 30, 2024

Tests failed for gradle-check-jdk17-windows!? 😮 Any help to repro these would be very welcomed, I've read the CI output and I'm not too sure what's failing / what's going on... especially since the tests seem to be the same tests that pass other CI runs and pass on my laptop! :-?

EDIT: ah, I think the error is the following but I'm not too sure:

java.nio.file.NoSuchFileException: C:\Users\circleci\AppData\Local\Temp\junit15390432245720982242\build\test.xml

I usually double-check the reports like pkl-gradle/build/reports/tests/test/index.html but I don't know if it's possible to access those via the CircleCI interface.

@holzensp
Copy link
Contributor

Regarding JUnit XML reports; this README is a good writeup.

As for the CI failure;

> Task :pkl-gradle:test

TestsTest > JUnit reports with error() FAILED
    java.nio.file.NoSuchFileException: C:\Users\circleci\AppData\Local\Temp\junit15390432245720982242\build\test.xml
        at java.base/sun.nio.fs.WindowsException.translateToIOException(WindowsException.java:85)
        at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:103)
        at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:108)
        at java.base/sun.nio.fs.WindowsFileSystemProvider.newByteChannel(WindowsFileSystemProvider.java:234)
        at java.base/java.nio.file.Files.newByteChannel(Files.java:379)
        at java.base/java.nio.file.Files.newByteChannel(Files.java:431)
        at java.base/java.nio.file.spi.FileSystemProvider.newInputStream(FileSystemProvider.java:420)
        at java.base/java.nio.file.Files.newInputStream(Files.java:159)
        at kotlin.io.path.PathsKt__PathReadWriteKt.readText(PathReadWrite.kt:155)
        at kotlin.io.path.PathsKt__PathReadWriteKt.readText$default(PathReadWrite.kt:154)
        at org.pkl.gradle.TestsTest.JUnit reports with error(TestsTest.kt:260)

TestsTest > full example with error() FAILED
    java.lang.AssertionError: 
    Expecting actual:
      "> Task :evalTest FAILED
    module test ? 25.0% pass [1 passed, 3 failed] (file:///file, line x)
      facts ?
        Error:
         Pkl Error 
        exception
    
        9 | throw("exception")
            ^^^^^^^^^^^^^^^^^^
        at test#facts["error"][#1] (file:///file, line x)
    
        3 | facts {
            ^^^^^^^
        at test#facts (file:///file, line x)
      examples ? 33.3% pass [1 passed, 2 failed]
        user 0 ?
        user 1 #0 ?
          (file:///file, line x)
          Expected: (file:///file, line x)
          new {
            name = "Pigeon"
            age = 40
          }
          Actual: (file:///file, line x)
          new {
            name = "Pigeon"
            age = 41
          }
        user 1 #1 ?
          (file:///file, line x)
          Expected: (file:///file, line x)
          new {
            name = "Parrot"
            age = 35
          }
          Actual: (file:///file, line x)
          new {
            name = "Welma"
            age = 35
          }

    FAILURE: Build failed with an exception.

    * What went wrong:
    Execution failed for task ':evalTest'.
    > Tests failed.

    * Try:
    > Run with --info or --debug option to get more log output.
    > Run with --scan to get full insights.
    > Get more help at https://help.gradle.org.

    * Exception is:
    org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':evalTest'.
        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.lambda$executeIfValid$1(ExecuteActionsTaskExecuter.java:130)
        at org.gradle.internal.Try$Failure.ifSuccessfulOrElse(Try.java:282)
        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeIfValid(ExecuteActionsTaskExecuter.java:128)
        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:116)
        at org.gradle.api.internal.tasks.execution.FinalizePropertiesTaskExecuter.execute(FinalizePropertiesTaskExecuter.java:46)
        at org.gradle.api.internal.tasks.execution.ResolveTaskExecutionModeExecuter.execute(ResolveTaskExecutionModeExecuter.java:51)
        at org.gradle.api.internal.tasks.execution.SkipTaskWithNoActionsExecuter.execute(SkipTaskWithNoActionsExecuter.java:57)
        at org.gradle.api.internal.tasks.execution.SkipOnlyIfTaskExecuter.execute(SkipOnlyIfTaskExecuter.java:74)
        at org.gradle.api.internal.tasks.execution.CatchExceptionTaskExecuter.execute(CatchExceptionTaskExecuter.java:36)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.executeTask(EventFiringTaskExecuter.java:77)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:55)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:52)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:200)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:195)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:157)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
        at org.gradle.internal.operations.DefaultBuildOperationExecutor.call(DefaultBuildOperationExecutor.java:73)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter.execute(EventFiringTaskExecuter.java:52)
        at org.gradle.execution.plan.LocalTaskNodeExecutor.execute(LocalTaskNodeExecutor.java:42)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:331)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:318)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.lambda$execute$0(DefaultTaskExecutionGraph.java:314)
        at org.gradle.internal.operations.CurrentBuildOperationRef.with(CurrentBuildOperationRef.java:80)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:314)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:303)
        at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.execute(DefaultPlanExecutor.java:463)
        at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.run(DefaultPlanExecutor.java:380)
        at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
        at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:47)
    Caused by: Tests failed.
        at org.pkl.cli.CliTestRunner.evalTest(CliTestRunner.kt:100)
        at org.pkl.cli.CliTestRunner.doRun(CliTestRunner.kt:39)
        at org.pkl.commons.cli.CliCommand.run(CliCommand.kt:39)
        at org.pkl.gradle.task.TestTask.doRunTask(TestTask.java:44)
        at org.pkl.gradle.task.ModulesTask.runTask(ModulesTask.java:155)
        at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
        at org.gradle.internal.reflect.JavaMethod.invoke(JavaMethod.java:125)
        at org.gradle.api.internal.project.taskfactory.StandardTaskAction.doExecute(StandardTaskAction.java:58)
        at org.gradle.api.internal.project.taskfactory.StandardTaskAction.execute(StandardTaskAction.java:51)
        at org.gradle.api.internal.project.taskfactory.StandardTaskAction.execute(StandardTaskAction.java:29)
        at org.gradle.api.internal.tasks.execution.TaskExecution$3.run(TaskExecution.java:244)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:29)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:26)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:157)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.run(DefaultBuildOperationRunner.java:47)
        at org.gradle.internal.operations.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:68)
        at org.gradle.api.internal.tasks.execution.TaskExecution.executeAction(TaskExecution.java:229)
        at org.gradle.api.internal.tasks.execution.TaskExecution.executeActions(TaskExecution.java:212)
        at org.gradle.api.internal.tasks.execution.TaskExecution.executeWithPreviousOutputFiles(TaskExecution.java:195)
        at org.gradle.api.internal.tasks.execution.TaskExecution.execute(TaskExecution.java:162)
        at org.gradle.internal.execution.steps.ExecuteStep.executeInternal(ExecuteStep.java:105)
        at org.gradle.internal.execution.steps.ExecuteStep.access$000(ExecuteStep.java:44)
        at org.gradle.internal.execution.steps.ExecuteStep$1.call(ExecuteStep.java:59)
        at org.gradle.internal.execution.steps.ExecuteStep$1.call(ExecuteStep.java:56)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:200)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:195)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:157)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
        at org.gradle.internal.operations.DefaultBuildOperationExecutor.call(DefaultBuildOperationExecutor.java:73)
        at org.gradle.internal.execution.steps.ExecuteStep.execute(ExecuteStep.java:56)
        at org.gradle.internal.execution.steps.ExecuteStep.execute(ExecuteStep.java:44)
        at org.gradle.internal.execution.steps.CancelExecutionStep.execute(CancelExecutionStep.java:41)
        at org.gradle.internal.execution.steps.TimeoutStep.executeWithoutTimeout(TimeoutStep.java:74)
        at org.gradle.internal.execution.steps.TimeoutStep.execute(TimeoutStep.java:55)
        at org.gradle.internal.execution.steps.PreCreateOutputParentsStep.execute(PreCreateOutputParentsStep.java:50)
        at org.gradle.internal.execution.steps.PreCreateOutputParentsStep.execute(PreCreateOutputParentsStep.java:28)
        at org.gradle.internal.execution.steps.RemovePreviousOutputsStep.execute(RemovePreviousOutputsStep.java:67)
        at org.gradle.internal.execution.steps.RemovePreviousOutputsStep.execute(RemovePreviousOutputsStep.java:37)
        at org.gradle.internal.execution.steps.BroadcastChangingOutputsStep.execute(BroadcastChangingOutputsStep.java:61)
        at org.gradle.internal.execution.steps.BroadcastChangingOutputsStep.execute(BroadcastChangingOutputsStep.java:26)
        at org.gradle.internal.execution.steps.CaptureOutputsAfterExecutionStep.execute(CaptureOutputsAfterExecutionStep.java:67)
        at org.gradle.internal.execution.steps.CaptureOutputsAfterExecutionStep.execute(CaptureOutputsAfterExecutionStep.java:45)
        at org.gradle.internal.execution.steps.ResolveInputChangesStep.execute(ResolveInputChangesStep.java:40)
        at org.gradle.internal.execution.steps.ResolveInputChangesStep.execute(ResolveInputChangesStep.java:29)
        at org.gradle.internal.execution.steps.BuildCacheStep.executeWithoutCache(BuildCacheStep.java:189)
        at org.gradle.internal.execution.steps.BuildCacheStep.lambda$execute$1(BuildCacheStep.java:75)
        at org.gradle.internal.Either$Right.fold(Either.java:175)
        at org.gradle.internal.execution.caching.CachingState.fold(CachingState.java:62)
        at org.gradle.internal.execution.steps.BuildCacheStep.execute(BuildCacheStep.java:73)
        at org.gradle.internal.execution.steps.BuildCacheStep.execute(BuildCacheStep.java:48)
        at org.gradle.internal.execution.steps.StoreExecutionStateStep.execute(StoreExecutionStateStep.java:46)
        at org.gradle.internal.execution.steps.StoreExecutionStateStep.execute(StoreExecutionStateStep.java:35)
        at org.gradle.internal.execution.steps.SkipUpToDateStep.executeBecause(SkipUpToDateStep.java:76)
        at org.gradle.internal.execution.steps.SkipUpToDateStep.lambda$execute$2(SkipUpToDateStep.java:54)
        at org.gradle.internal.execution.steps.SkipUpToDateStep.execute(SkipUpToDateStep.java:54)
        at org.gradle.internal.execution.steps.SkipUpToDateStep.execute(SkipUpToDateStep.java:36)
        at org.gradle.internal.execution.steps.legacy.MarkSnapshottingInputsFinishedStep.execute(MarkSnapshottingInputsFinishedStep.java:37)
        at org.gradle.internal.execution.steps.legacy.MarkSnapshottingInputsFinishedStep.execute(MarkSnapshottingInputsFinishedStep.java:27)
        at org.gradle.internal.execution.steps.ResolveIncrementalCachingStateStep.executeDelegate(ResolveIncrementalCachingStateStep.java:49)
        at org.gradle.internal.execution.steps.ResolveIncrementalCachingStateStep.executeDelegate(ResolveIncrementalCachingStateStep.java:27)
        at org.gradle.internal.execution.steps.AbstractResolveCachingStateStep.execute(AbstractResolveCachingStateStep.java:71)
        at org.gradle.internal.execution.steps.AbstractResolveCachingStateStep.execute(AbstractResolveCachingStateStep.java:39)
        at org.gradle.internal.execution.steps.ResolveChangesStep.execute(ResolveChangesStep.java:65)
        at org.gradle.internal.execution.steps.ResolveChangesStep.execute(ResolveChangesStep.java:36)
        at org.gradle.internal.execution.steps.ValidateStep.execute(ValidateStep.java:106)
        at org.gradle.internal.execution.steps.ValidateStep.execute(ValidateStep.java:55)
        at org.gradle.internal.execution.steps.AbstractCaptureStateBeforeExecutionStep.execute(AbstractCaptureStateBeforeExecutionStep.java:64)
        at org.gradle.internal.execution.steps.AbstractCaptureStateBeforeExecutionStep.execute(AbstractCaptureStateBeforeExecutionStep.java:43)
        at org.gradle.internal.execution.steps.AbstractSkipEmptyWorkStep.executeWithNonEmptySources(AbstractSkipEmptyWorkStep.java:125)
        at org.gradle.internal.execution.steps.AbstractSkipEmptyWorkStep.execute(AbstractSkipEmptyWorkStep.java:56)
        at org.gradle.internal.execution.steps.AbstractSkipEmptyWorkStep.execute(AbstractSkipEmptyWorkStep.java:36)
        at org.gradle.internal.execution.steps.legacy.MarkSnapshottingInputsStartedStep.execute(MarkSnapshottingInputsStartedStep.java:38)
        at org.gradle.internal.execution.steps.LoadPreviousExecutionStateStep.execute(LoadPreviousExecutionStateStep.java:36)
        at org.gradle.internal.execution.steps.LoadPreviousExecutionStateStep.execute(LoadPreviousExecutionStateStep.java:23)
        at org.gradle.internal.execution.steps.HandleStaleOutputsStep.execute(HandleStaleOutputsStep.java:75)
        at org.gradle.internal.execution.steps.HandleStaleOutputsStep.execute(HandleStaleOutputsStep.java:41)
        at org.gradle.internal.execution.steps.AssignMutableWorkspaceStep.lambda$execute$0(AssignMutableWorkspaceStep.java:35)
        at org.gradle.api.internal.tasks.execution.TaskExecution$4.withWorkspace(TaskExecution.java:289)
        at org.gradle.internal.execution.steps.AssignMutableWorkspaceStep.execute(AssignMutableWorkspaceStep.java:31)
        at org.gradle.internal.execution.steps.AssignMutableWorkspaceStep.execute(AssignMutableWorkspaceStep.java:22)
        at org.gradle.internal.execution.steps.ChoosePipelineStep.execute(ChoosePipelineStep.java:40)
        at org.gradle.internal.execution.steps.ChoosePipelineStep.execute(ChoosePipelineStep.java:23)
        at org.gradle.internal.execution.steps.ExecuteWorkBuildOperationFiringStep.lambda$execute$2(ExecuteWorkBuildOperationFiringStep.java:67)
        at org.gradle.internal.execution.steps.ExecuteWorkBuildOperationFiringStep.execute(ExecuteWorkBuildOperationFiringStep.java:67)
        at org.gradle.internal.execution.steps.ExecuteWorkBuildOperationFiringStep.execute(ExecuteWorkBuildOperationFiringStep.java:39)
        at org.gradle.internal.execution.steps.IdentityCacheStep.execute(IdentityCacheStep.java:46)
        at org.gradle.internal.execution.steps.IdentityCacheStep.execute(IdentityCacheStep.java:34)
        at org.gradle.internal.execution.steps.IdentifyStep.execute(IdentifyStep.java:48)
        at org.gradle.internal.execution.steps.IdentifyStep.execute(IdentifyStep.java:35)
        at org.gradle.internal.execution.impl.DefaultExecutionEngine$1.execute(DefaultExecutionEngine.java:61)
        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeIfValid(ExecuteActionsTaskExecuter.java:127)
        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:116)
        at org.gradle.api.internal.tasks.execution.FinalizePropertiesTaskExecuter.execute(FinalizePropertiesTaskExecuter.java:46)
        at org.gradle.api.internal.tasks.execution.ResolveTaskExecutionModeExecuter.execute(ResolveTaskExecutionModeExecuter.java:51)
        at org.gradle.api.internal.tasks.execution.SkipTaskWithNoActionsExecuter.execute(SkipTaskWithNoActionsExecuter.java:57)
        at org.gradle.api.internal.tasks.execution.SkipOnlyIfTaskExecuter.execute(SkipOnlyIfTaskExecuter.java:74)
        at org.gradle.api.internal.tasks.execution.CatchExceptionTaskExecuter.execute(CatchExceptionTaskExecuter.java:36)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.executeTask(EventFiringTaskExecuter.java:77)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:55)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:52)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:200)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:195)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:157)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
        at org.gradle.internal.operations.DefaultBuildOperationExecutor.call(DefaultBuildOperationExecutor.java:73)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter.execute(EventFiringTaskExecuter.java:52)
        at org.gradle.execution.plan.LocalTaskNodeExecutor.execute(LocalTaskNodeExecutor.java:42)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:331)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:318)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.lambda$execute$0(DefaultTaskExecutionGraph.java:314)
        at org.gradle.internal.operations.CurrentBuildOperationRef.with(CurrentBuildOperationRef.java:80)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:314)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:303)
        at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.execute(DefaultPlanExecutor.java:463)
        at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.run(DefaultPlanExecutor.java:380)
        at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
        at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:47)


    BUILD FAILED in 113ms
    1 actionable task: 1 executed
    "
    to start with:
      "> Task :evalTest FAILED
    module test ? 25.0% pass [1 passed, 3 failed] (file:///file, line x)
      facts ?
        Error:
         Pkl Error 
        exception
    
        9 | throw("exception")
            ^^^^^^^^^^^^^^^^^^
        at test#facts["error"][#1] (file:///file, line x)
    
        3 | facts {
            ^^^^^^^
        at test#facts (file:///file, line x)
      examples ? 33.3% pass [1 passed, 2 failed]
        user 0 ?
        user 1 #0 ?
          (file:///file, line x)
          Expected: (file:///file, line x)
          new {
            name = "Pigeon"
            age = 40
          }
          Actual: (file:///file, line x)
          new {
            name = "Pigeon"
            age = 41
          }
        user 1 #1 ?
          (file:///file, line x)
          Expected: (file:///file, line x)
          new {
            name = "Parrot"
            age = 35
          }
          Actual: (file:///file, line x)
          new {
            name = "Welma"
            age = 35
          }"
        at org.pkl.gradle.TestsTest.full example with error(TestsTest.kt:144)

TestsTest > full example() FAILED
    java.lang.AssertionError: 
    Expecting actual:
      "> Task :evalTest FAILED
    pkl: TRACE: 8 = 8 (file:///file, line x)
    module test ? 50.0% pass [3 passed, 3 failed] (file:///file, line x)
      facts ? 66.7% pass [2 passed, 1 failed]
        sum numbers ? 100.0% pass [2 passed]
        divide numbers ? 100.0% pass [2 passed]
        fail ? 0.0% pass [0 passed, 2 failed]
          4 == 9 ? (file:///file, line x)
          "foo" == "bar" ? (file:///file, line x)
      examples ? 33.3% pass [1 passed, 2 failed]
        user 0 ?
        user 1 #0 ?
          (file:///file, line x)
          Expected: (file:///file, line x)
          new {
            name = "Pigeon"
            age = 40
          }
          Actual: (file:///file, line x)
          new {
            name = "Pigeon"
            age = 41
          }
        user 1 #1 ?
          (file:///file, line x)
          Expected: (file:///file, line x)
          new {
            name = "Parrot"
            age = 35
          }
          Actual: (file:///file, line x)
          new {
            name = "Welma"
            age = 35
          }

    FAILURE: Build failed with an exception.

    * What went wrong:
    Execution failed for task ':evalTest'.
    > Tests failed.

    * Try:
    > Run with --info or --debug option to get more log output.
    > Run with --scan to get full insights.
    > Get more help at https://help.gradle.org.

    * Exception is:
    org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':evalTest'.
        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.lambda$executeIfValid$1(ExecuteActionsTaskExecuter.java:130)
        at org.gradle.internal.Try$Failure.ifSuccessfulOrElse(Try.java:282)
        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeIfValid(ExecuteActionsTaskExecuter.java:128)
        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:116)
        at org.gradle.api.internal.tasks.execution.FinalizePropertiesTaskExecuter.execute(FinalizePropertiesTaskExecuter.java:46)
        at org.gradle.api.internal.tasks.execution.ResolveTaskExecutionModeExecuter.execute(ResolveTaskExecutionModeExecuter.java:51)
        at org.gradle.api.internal.tasks.execution.SkipTaskWithNoActionsExecuter.execute(SkipTaskWithNoActionsExecuter.java:57)
        at org.gradle.api.internal.tasks.execution.SkipOnlyIfTaskExecuter.execute(SkipOnlyIfTaskExecuter.java:74)
        at org.gradle.api.internal.tasks.execution.CatchExceptionTaskExecuter.execute(CatchExceptionTaskExecuter.java:36)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.executeTask(EventFiringTaskExecuter.java:77)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:55)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:52)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:200)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:195)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:157)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
        at org.gradle.internal.operations.DefaultBuildOperationExecutor.call(DefaultBuildOperationExecutor.java:73)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter.execute(EventFiringTaskExecuter.java:52)
        at org.gradle.execution.plan.LocalTaskNodeExecutor.execute(LocalTaskNodeExecutor.java:42)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:331)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:318)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.lambda$execute$0(DefaultTaskExecutionGraph.java:314)
        at org.gradle.internal.operations.CurrentBuildOperationRef.with(CurrentBuildOperationRef.java:80)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:314)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:303)
        at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.execute(DefaultPlanExecutor.java:463)
        at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.run(DefaultPlanExecutor.java:380)
        at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
        at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:47)
    Caused by: Tests failed.
        at org.pkl.cli.CliTestRunner.evalTest(CliTestRunner.kt:100)
        at org.pkl.cli.CliTestRunner.doRun(CliTestRunner.kt:39)
        at org.pkl.commons.cli.CliCommand.run(CliCommand.kt:39)
        at org.pkl.gradle.task.TestTask.doRunTask(TestTask.java:44)
        at org.pkl.gradle.task.ModulesTask.runTask(ModulesTask.java:155)
        at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
        at org.gradle.internal.reflect.JavaMethod.invoke(JavaMethod.java:125)
        at org.gradle.api.internal.project.taskfactory.StandardTaskAction.doExecute(StandardTaskAction.java:58)
        at org.gradle.api.internal.project.taskfactory.StandardTaskAction.execute(StandardTaskAction.java:51)
        at org.gradle.api.internal.project.taskfactory.StandardTaskAction.execute(StandardTaskAction.java:29)
        at org.gradle.api.internal.tasks.execution.TaskExecution$3.run(TaskExecution.java:244)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:29)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:26)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:157)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.run(DefaultBuildOperationRunner.java:47)
        at org.gradle.internal.operations.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:68)
        at org.gradle.api.internal.tasks.execution.TaskExecution.executeAction(TaskExecution.java:229)
        at org.gradle.api.internal.tasks.execution.TaskExecution.executeActions(TaskExecution.java:212)
        at org.gradle.api.internal.tasks.execution.TaskExecution.executeWithPreviousOutputFiles(TaskExecution.java:195)
        at org.gradle.api.internal.tasks.execution.TaskExecution.execute(TaskExecution.java:162)
        at org.gradle.internal.execution.steps.ExecuteStep.executeInternal(ExecuteStep.java:105)
        at org.gradle.internal.execution.steps.ExecuteStep.access$000(ExecuteStep.java:44)
        at org.gradle.internal.execution.steps.ExecuteStep$1.call(ExecuteStep.java:59)
        at org.gradle.internal.execution.steps.ExecuteStep$1.call(ExecuteStep.java:56)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:200)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:195)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:157)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
        at org.gradle.internal.operations.DefaultBuildOperationExecutor.call(DefaultBuildOperationExecutor.java:73)
        at org.gradle.internal.execution.steps.ExecuteStep.execute(ExecuteStep.java:56)
        at org.gradle.internal.execution.steps.ExecuteStep.execute(ExecuteStep.java:44)
        at org.gradle.internal.execution.steps.CancelExecutionStep.execute(CancelExecutionStep.java:41)
        at org.gradle.internal.execution.steps.TimeoutStep.executeWithoutTimeout(TimeoutStep.java:74)
        at org.gradle.internal.execution.steps.TimeoutStep.execute(TimeoutStep.java:55)
        at org.gradle.internal.execution.steps.PreCreateOutputParentsStep.execute(PreCreateOutputParentsStep.java:50)
        at org.gradle.internal.execution.steps.PreCreateOutputParentsStep.execute(PreCreateOutputParentsStep.java:28)
        at org.gradle.internal.execution.steps.RemovePreviousOutputsStep.execute(RemovePreviousOutputsStep.java:67)
        at org.gradle.internal.execution.steps.RemovePreviousOutputsStep.execute(RemovePreviousOutputsStep.java:37)
        at org.gradle.internal.execution.steps.BroadcastChangingOutputsStep.execute(BroadcastChangingOutputsStep.java:61)
        at org.gradle.internal.execution.steps.BroadcastChangingOutputsStep.execute(BroadcastChangingOutputsStep.java:26)
        at org.gradle.internal.execution.steps.CaptureOutputsAfterExecutionStep.execute(CaptureOutputsAfterExecutionStep.java:67)
        at org.gradle.internal.execution.steps.CaptureOutputsAfterExecutionStep.execute(CaptureOutputsAfterExecutionStep.java:45)
        at org.gradle.internal.execution.steps.ResolveInputChangesStep.execute(ResolveInputChangesStep.java:40)
        at org.gradle.internal.execution.steps.ResolveInputChangesStep.execute(ResolveInputChangesStep.java:29)
        at org.gradle.internal.execution.steps.BuildCacheStep.executeWithoutCache(BuildCacheStep.java:189)
        at org.gradle.internal.execution.steps.BuildCacheStep.lambda$execute$1(BuildCacheStep.java:75)
        at org.gradle.internal.Either$Right.fold(Either.java:175)
        at org.gradle.internal.execution.caching.CachingState.fold(CachingState.java:62)
        at org.gradle.internal.execution.steps.BuildCacheStep.execute(BuildCacheStep.java:73)
        at org.gradle.internal.execution.steps.BuildCacheStep.execute(BuildCacheStep.java:48)
        at org.gradle.internal.execution.steps.StoreExecutionStateStep.execute(StoreExecutionStateStep.java:46)
        at org.gradle.internal.execution.steps.StoreExecutionStateStep.execute(StoreExecutionStateStep.java:35)
        at org.gradle.internal.execution.steps.SkipUpToDateStep.executeBecause(SkipUpToDateStep.java:76)
        at org.gradle.internal.execution.steps.SkipUpToDateStep.lambda$execute$2(SkipUpToDateStep.java:54)
        at org.gradle.internal.execution.steps.SkipUpToDateStep.execute(SkipUpToDateStep.java:54)
        at org.gradle.internal.execution.steps.SkipUpToDateStep.execute(SkipUpToDateStep.java:36)
        at org.gradle.internal.execution.steps.legacy.MarkSnapshottingInputsFinishedStep.execute(MarkSnapshottingInputsFinishedStep.java:37)
        at org.gradle.internal.execution.steps.legacy.MarkSnapshottingInputsFinishedStep.execute(MarkSnapshottingInputsFinishedStep.java:27)
        at org.gradle.internal.execution.steps.ResolveIncrementalCachingStateStep.executeDelegate(ResolveIncrementalCachingStateStep.java:49)
        at org.gradle.internal.execution.steps.ResolveIncrementalCachingStateStep.executeDelegate(ResolveIncrementalCachingStateStep.java:27)
        at org.gradle.internal.execution.steps.AbstractResolveCachingStateStep.execute(AbstractResolveCachingStateStep.java:71)
        at org.gradle.internal.execution.steps.AbstractResolveCachingStateStep.execute(AbstractResolveCachingStateStep.java:39)
        at org.gradle.internal.execution.steps.ResolveChangesStep.execute(ResolveChangesStep.java:65)
        at org.gradle.internal.execution.steps.ResolveChangesStep.execute(ResolveChangesStep.java:36)
        at org.gradle.internal.execution.steps.ValidateStep.execute(ValidateStep.java:106)
        at org.gradle.internal.execution.steps.ValidateStep.execute(ValidateStep.java:55)
        at org.gradle.internal.execution.steps.AbstractCaptureStateBeforeExecutionStep.execute(AbstractCaptureStateBeforeExecutionStep.java:64)
        at org.gradle.internal.execution.steps.AbstractCaptureStateBeforeExecutionStep.execute(AbstractCaptureStateBeforeExecutionStep.java:43)
        at org.gradle.internal.execution.steps.AbstractSkipEmptyWorkStep.executeWithNonEmptySources(AbstractSkipEmptyWorkStep.java:125)
        at org.gradle.internal.execution.steps.AbstractSkipEmptyWorkStep.execute(AbstractSkipEmptyWorkStep.java:56)
        at org.gradle.internal.execution.steps.AbstractSkipEmptyWorkStep.execute(AbstractSkipEmptyWorkStep.java:36)
        at org.gradle.internal.execution.steps.legacy.MarkSnapshottingInputsStartedStep.execute(MarkSnapshottingInputsStartedStep.java:38)
        at org.gradle.internal.execution.steps.LoadPreviousExecutionStateStep.execute(LoadPreviousExecutionStateStep.java:36)
        at org.gradle.internal.execution.steps.LoadPreviousExecutionStateStep.execute(LoadPreviousExecutionStateStep.java:23)
        at org.gradle.internal.execution.steps.HandleStaleOutputsStep.execute(HandleStaleOutputsStep.java:75)
        at org.gradle.internal.execution.steps.HandleStaleOutputsStep.execute(HandleStaleOutputsStep.java:41)
        at org.gradle.internal.execution.steps.AssignMutableWorkspaceStep.lambda$execute$0(AssignMutableWorkspaceStep.java:35)
        at org.gradle.api.internal.tasks.execution.TaskExecution$4.withWorkspace(TaskExecution.java:289)
        at org.gradle.internal.execution.steps.AssignMutableWorkspaceStep.execute(AssignMutableWorkspaceStep.java:31)
        at org.gradle.internal.execution.steps.AssignMutableWorkspaceStep.execute(AssignMutableWorkspaceStep.java:22)
        at org.gradle.internal.execution.steps.ChoosePipelineStep.execute(ChoosePipelineStep.java:40)
        at org.gradle.internal.execution.steps.ChoosePipelineStep.execute(ChoosePipelineStep.java:23)
        at org.gradle.internal.execution.steps.ExecuteWorkBuildOperationFiringStep.lambda$execute$2(ExecuteWorkBuildOperationFiringStep.java:67)
        at org.gradle.internal.execution.steps.ExecuteWorkBuildOperationFiringStep.execute(ExecuteWorkBuildOperationFiringStep.java:67)
        at org.gradle.internal.execution.steps.ExecuteWorkBuildOperationFiringStep.execute(ExecuteWorkBuildOperationFiringStep.java:39)
        at org.gradle.internal.execution.steps.IdentityCacheStep.execute(IdentityCacheStep.java:46)
        at org.gradle.internal.execution.steps.IdentityCacheStep.execute(IdentityCacheStep.java:34)
        at org.gradle.internal.execution.steps.IdentifyStep.execute(IdentifyStep.java:48)
        at org.gradle.internal.execution.steps.IdentifyStep.execute(IdentifyStep.java:35)
        at org.gradle.internal.execution.impl.DefaultExecutionEngine$1.execute(DefaultExecutionEngine.java:61)
        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeIfValid(ExecuteActionsTaskExecuter.java:127)
        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:116)
        at org.gradle.api.internal.tasks.execution.FinalizePropertiesTaskExecuter.execute(FinalizePropertiesTaskExecuter.java:46)
        at org.gradle.api.internal.tasks.execution.ResolveTaskExecutionModeExecuter.execute(ResolveTaskExecutionModeExecuter.java:51)
        at org.gradle.api.internal.tasks.execution.SkipTaskWithNoActionsExecuter.execute(SkipTaskWithNoActionsExecuter.java:57)
        at org.gradle.api.internal.tasks.execution.SkipOnlyIfTaskExecuter.execute(SkipOnlyIfTaskExecuter.java:74)
        at org.gradle.api.internal.tasks.execution.CatchExceptionTaskExecuter.execute(CatchExceptionTaskExecuter.java:36)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.executeTask(EventFiringTaskExecuter.java:77)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:55)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:52)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:200)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:195)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:157)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
        at org.gradle.internal.operations.DefaultBuildOperationExecutor.call(DefaultBuildOperationExecutor.java:73)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter.execute(EventFiringTaskExecuter.java:52)
        at org.gradle.execution.plan.LocalTaskNodeExecutor.execute(LocalTaskNodeExecutor.java:42)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:331)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:318)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.lambda$execute$0(DefaultTaskExecutionGraph.java:314)
        at org.gradle.internal.operations.CurrentBuildOperationRef.with(CurrentBuildOperationRef.java:80)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:314)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:303)
        at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.execute(DefaultPlanExecutor.java:463)
        at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.run(DefaultPlanExecutor.java:380)
        at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
        at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:47)


    BUILD FAILED in 139ms
    1 actionable task: 1 executed
    "
    to start with:
      "> Task :evalTest FAILED
    pkl: TRACE: 8 = 8 (file:///file, line x)
    module test ? 50.0% pass [3 passed, 3 failed] (file:///file, line x)
      facts ? 66.7% pass [2 passed, 1 failed]
        sum numbers ? 100.0% pass [2 passed]
        divide numbers ? 100.0% pass [2 passed]
        fail ? 0.0% pass [0 passed, 2 failed]
          4 == 9 ? (file:///file, line x)
          "foo" == "bar" ? (file:///file, line x)
      examples ? 33.3% pass [1 passed, 2 failed]
        user 0 ?
        user 1 #0 ?
          (file:///file, line x)
          Expected: (file:///file, line x)
          new {
            name = "Pigeon"
            age = 40
          }
          Actual: (file:///file, line x)
          new {
            name = "Pigeon"
            age = 41
          }
        user 1 #1 ?
          (file:///file, line x)
          Expected: (file:///file, line x)
          new {
            name = "Parrot"
            age = 35
          }
          Actual: (file:///file, line x)
          new {
            name = "Welma"
            age = 35
          }"
        at org.pkl.gradle.TestsTest.full example(TestsTest.kt:76)

Gradle Test Executor 17 finished executing tests.

> Task :pkl-gradle:test FAILED

@jjmaestro
Copy link
Contributor Author

I've rebased the branch and fixed the conflicts. @holzensp thanks for the trace! I'll have a look and see if I can spot what's going on in there 😄

@jjmaestro
Copy link
Contributor Author

OK, I think I've fixed the tests in Windows.

I've tested it in a Windows 11 VM, I think the issue was with the .trimIndent() not preserving the original line endings.

Hopefully it'll all run fine now!

@jjmaestro
Copy link
Contributor Author

jjmaestro commented Aug 7, 2024

Wow, I don't know WTH is going on with the commits! I've been trying to update the branch and I think I've messed it up... 😬 gonna try & clean it 😓

EDIT: ah, looks like things are fine now, those commits are in main and my commit is now rebased on top of them, without that merge-commit.

@jjmaestro
Copy link
Contributor Author

@holzensp / @bioball do the CI jobs stay on "pending" until one of you approves it? If so, could you let it run and see if it comes all green?

Thanks!

@jjmaestro
Copy link
Contributor Author

@holzensp / @bioball looks like all tests have passed! :D can I get a review? 🙏 😄 Thanks!!

@bioball
Copy link
Contributor

bioball commented Aug 13, 2024

Will take a look this week!

Copy link
Contributor

@bioball bioball left a comment

Choose a reason for hiding this comment

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

Thanks for the PR! Sorry it took so long for me to take a look at this.

I see what you're doing here, but I'm concerned that this output overall adds too much noise and is less helpful.

Here is a sample test report from your changes:

module test ❌ 50.0% pass [1 passed, 1 failed] (/Users/danielchao/code/apple/pkl/.dan-scripts/test.pkl:1)
  facts ❌ 50.0% pass [1 passed, 1 failed]
    math ❌ 50.0% pass [1 passed, 1 failed]
      1 == 2 ❌ (/Users/danielchao/code/apple/pkl/.dan-scripts/test.pkl:5)
    double ✅ 100.0% pass [2 passed]

Some thoughts about this output:

  • There's too many emojis; we don't need to show ❌ and ✅ next to the module name or the test name
  • It's confusing that the numbers don't add up. How come the overall module tells me 1 passed and 1 failed, but at the bottom it tells me 2?

I'm thinking that this output would be much nicer:

module test (/Users/danielchao/code/apple/pkl/.dan-scripts/test.pkl:1)
  facts
    math ❌
      1 == 2 (/Users/danielchao/code/apple/pkl/.dan-scripts/test.pkl:5)
    double ✅

1 passed, 1 failed (50%)

What I'm thinking:

  • The number of tests is the number of fact/example entries (the things that are named).
  • We show summary at the bottom of the test report (instead of next to each item).
  • We only show ❌ or ✅ next to the test.

BTW, the ✅ emoji is kind of intense. I wonder if we should switch to the ✘ and ✔ characters instead, with ANSI color coding. But this will require the error coloring branch (https://github.com/apple/pkl/tree/error-coloring) to be merged.

@jjmaestro
Copy link
Contributor Author

Thanks for the review!

I see what you're doing here, but I'm concerned that this output overall adds too much noise and is less helpful.

Agreed, it can definitely be improved!

  • There's too many emojis; we don't need to show ❌ and ✅ next to the module name or the test name

I was thinking that it would be good to know what parts pass and what parts fail, and to "get it all green", but yeah, it can be a bit too much.

It's confusing that the numbers don't add up. How come the overall module tells me 1 passed and 1 failed, but at the bottom it tells me 2?

Yeah, it's tests vs asserts in the tests. I agree, it's confusing.

I'm thinking that this output would be much nicer:

module test (/Users/danielchao/code/apple/pkl/.dan-scripts/test.pkl:1)
  facts
    math ❌
      1 == 2 (/Users/danielchao/code/apple/pkl/.dan-scripts/test.pkl:5)
    double ✅

1 passed, 1 failed (50%)

That's much better :) I still think it would be nice to know how many asserts pass & fail as wel. Maybe there could be a "verbose flag" that shows those... dunno, we'll see!

What I'm thinking:

  • The number of tests is the number of fact/example entries (the things that are named).
  • We show summary at the bottom of the test report (instead of next to each item).
  • We only show ❌ or ✅ next to the test.

Agreed, that's a good plan :) I'll give it a try!

BTW, the ✅ emoji is kind of intense. I wonder if we should switch to the ✘ and ✔ characters instead, with ANSI color coding. But this will require the error coloring branch (https://github.com/apple/pkl/tree/error-coloring) to be merged.

For sure 😅 Once that's merged I agree it would probably be better to not "hardcode the colors via emojis" and use the other UTF characters + ANSI coloring. Do you have an ETA for that code merging?

Alright, I'll give this PR another spin and address your comments!

@jjmaestro
Copy link
Contributor Author

@bioball I've pushed a bunch of changes that I think will addressed all your comments.

The only additional thing is that I've also added the assertion count to the the summary line. Let me know what you think!

Here's the output of a "real life project" I'm working on with a bunch of "forced failures" to see how it all would look like:

# Pkl 0.27.0-dev+45738dd (macOS 14.5, native)

module pkl.dockerfile.tests.Dockelfile
  facts
    ✅ VariableName
    ✅ Variable
    ✅ Platform
    ✅ ImageTag
    ❌ Digest
       "0000" is dkl.Digest (pkl-docker/pkl.dockerfile/tests/Dockelfile.pkl:46:5)
       "sha256:" + "foo".sha256.replaceFirst("f", "g") is dkl.Digest (pkl-docker/pkl.dockerfile/tests/Dockelfile.pkl:47:5)
    ✅ ImageDigest
    ✅ ImageTagPinned
    ✅ HereDocDelim
    ✅ HereDoc
    ✅ UnixUserGroup
    ✅ OctalDigit
    ✅ UnixPerm
    ✅ UnixSignal
    ✅ Port
    ✅ dfmt
    ✅ HealthcheckDuration
    ✅ HardcodedDirective
    ✅ Directive
    ✅ Arg
    ✅ Image
    ✅ From
    ✅ CommandShell
    ✅ ShellDelim
    ✅ CommandShellGrouped
    ✅ CommandExec
    ✅ Command
    ✅ Shell
    ✅ Cmd
    ✅ Healthcheck
    ✅ Add
    ✅ Copy
    ✅ Run
    ✅ KV
    ✅ KVs
    ✅ Label
    ✅ Env
    ✅ Expose
    ✅ Entrypoint
    ✅ Volume
    ✅ User
    ✅ Workdir
    ✅ OnBuild
    ✅ StopSignal
  examples
    ❌ ex1.Dockerfile
       (pkl-docker/pkl.dockerfile/tests/Dockelfile.pkl:672:7)
       Expected: (pkl-docker/pkl.dockerfile/tests/Dockelfile.pkl-expected.pcf:3:5)
       """
         # syntax=docker/dockerfile:1-labs
         
         
         FROM scratcX
         
         """
       Actual: (pkl-docker/pkl.dockerfile/tests/Dockelfile.pkl-actual.pcf:3:5)
       """
         # syntax=docker/dockerfile:1-labs
         
         
         FROM scratch
         
         """
    ✅ ex2.Dockerfile
❌ 95.6% tests pass [2/45 failed], 98.3% asserts pass [3/178 failed]

module pkl.dockerfile.tests.stringify
  facts
    ✅ listify
    ✅ listifyOrNull
    ✅ Stringy
    ✅ Stringies
    ✅ LStringies
    ✅ sPrefix
    ✅ sDelim
    ✅ sKEQVs
✅ 100.0% tests pass [8 passed], 100.0% asserts pass [29 passed]

Thanks!

@jjmaestro jjmaestro requested a review from bioball August 19, 2024 21:55
@jjmaestro
Copy link
Contributor Author

@bioball what do you think about the last output I pasted? Is it now OK (re. your comment about the output having noise and maybe not being helpful)?

Thanks!

@bioball
Copy link
Contributor

bioball commented Oct 11, 2024

Apologies for dropping the ball here! It's not an excuse, but the LSP work has sucked all my attention.

I think your revised output looks pretty good! There's some merge conflicts to deal with; apologies for that. Can you address those? Once you do, I'll review this again and let's work towards getting this merged 😁

@jjmaestro
Copy link
Contributor Author

@bioball no worries :) I'll go over the merge conflicts and will update the PR. Thanks a ton!

@jjmaestro jjmaestro force-pushed the improve-testing branch 2 times, most recently from b8d27d9 to c74d92a Compare October 18, 2024 23:12
@jjmaestro
Copy link
Contributor Author

jjmaestro commented Oct 18, 2024

@bioball OK, I finally managed to rebase my branch against latest main. I've made a few more minor changes that clean the output quite a bit and I changed a couple tests with examples to have a wider test (e.g. one example with one assert that passes and then one example with one assert that passes and another that fails).

Please review the branch when you have a minute and let me know if there's anything I should change!

@jjmaestro
Copy link
Contributor Author

OK, I've added back an extra line at the end of the modules but only if there's more than one module in the test. Now, the output matches the previous output but I don't have to modify the expected test outputs.

Here's the "new look", like the last one I pasted but I've removed an additional Error: line before the –– Pkl Error –– because IMHO it was redundant:

# Pkl 0.27.0-dev+c74d92a (macOS 14.6.1, native)

module pkl.dockerfile.tests.Dockelfile
  facts
    ✅ VariableName
    ✅ Variable
    ✅ Platform
    ✅ ImageTag
    ❌ Digest
       ("0000" is dkl.Digest) (pkl-docker/pkl.dockerfile/tests/Dockelfile.pkl:46:5)
       ("sha256:" + "foo".sha256.replaceFirst("f", "g") is dkl.Digest) (pkl-docker/pkl.dockerfile/tests/Dockelfile.pkl:47:5)
    ✅ ImageDigest
    ✅ ImageTagPinned
    ✅ HereDocDelim
    ❌ HereDoc
       –– Pkl Error ––
       some error

       130 | throw("some error")
             ^^^^^^^^^^^^^^^^^^^
       at pkl.dockerfile.tests.Dockelfile#facts["HereDoc"][#5] (pkl-docker/pkl.dockerfile/tests/Dockelfile.pkl:130:5)
    ✅ UnixUserGroup
    ✅ OctalDigit
    ✅ UnixPerm
    ✅ UnixSignal
    ✅ Port
    ✅ dfmt
    ✅ HealthcheckDuration
    ✅ HardcodedDirective
    ✅ Directive
    ✅ Arg
    ✅ Image
    ✅ From
    ✅ CommandShell
    ✅ ShellDelim
    ✅ CommandShellGrouped
    ✅ CommandExec
    ✅ Command
    ✅ Shell
    ✅ Cmd
    ✅ Healthcheck
    ✅ Add
    ✅ Copy
    ✅ Run
    ✅ KV
    ✅ KVs
    ✅ Label
    ✅ Env
    ✅ Expose
    ✅ Entrypoint
    ✅ Volume
    ✅ User
    ✅ Workdir
    ✅ OnBuild
    ✅ StopSignal
  examples
    ❌ ex1.Dockerfile
       (pkl-docker/pkl.dockerfile/tests/Dockelfile.pkl:674:7)
       Expected: (pkl-docker/pkl.dockerfile/tests/Dockelfile.pkl-expected.pcf:3:5)
       """
         # syntax=docker/dockerfile:1-labs
         
         
         FROM debian12
         
         """
       Actual: (pkl-docker/pkl.dockerfile/tests/Dockelfile.pkl-actual.pcf:3:5)
       """
         # syntax=docker/dockerfile:1-labs
         
         
         FROM scratch
         
         """
    ✅ ex2.Dockerfile
❌ 93.3% tests pass [3/45 failed], 97.8% asserts pass [4/181 failed]

module pkl.dockerfile.tests.stringify
  facts
    ✅ listify
    ✅ listifyOrNull
    ✅ Stringy
    ✅ Stringies
    ✅ LStringies
    ✅ sPrefix
    ✅ sDelim
    ✅ sKEQVs
✅ 100.0% tests pass [8 passed], 100.0% asserts pass [29 passed]

And this is how it looks with a module-level error:

Error evaluating module pkl-docker/pkl.dockerfile/tests/Dockelfile.pkl:
–– Pkl Error ––
Mismatched input: `"`. Expected one of: `{`, `=`, `:`

5 | amendz "pkl:test"
           ^
at Dockelfile (pkl-docker/pkl.dockerfile/tests/Dockelfile.pkl:5:8)

module pkl.dockerfile.tests.stringify
  facts
    ✅ listify
    ✅ listifyOrNull
    ✅ Stringy
    ✅ Stringies
    ✅ LStringies
    ✅ sPrefix
    ✅ sDelim
    ✅ sKEQVs
✅ 100.0% tests pass [8 passed], 100.0% asserts pass [29 passed]

* TestRunner: separated the running of each "section" of a test into its own try-catch so that we can evaluate each section separately even if another section failed (e.g. evaluate all examples even if one test in "facts" throws an exception). I've added tests that exercise this "new feature".

* TestResults: refactored it a bit so that TestResults now has the three sections and the old TestResults becomes TestSectionResults. Also, TestSectionResults now has only *one* error, since that's what we get when there's an exception evaluating the Pkl code.

* SimpleReport:
  * Separated reporting of each test section so that it's easy to see which facts or which examples fail.

  * Added a "stats line" to the module level that reports how many tests and assertions pass/fail. Note that examples are equivalent to "one test with an 'all-or-nothing assertion' so they are counted as 1 test with 1 assertion. Similarly errors will also count as "one test with one assertion failed" because, at the moment, individual errors are not being surfaced individually. Instead, only the first error is being surfaced and thus it masks the rest of the tests and assertions in the test section where it happens.

  * For the examples converted to tests, when multiple examples share the same name I've also added a counter ("# 0" and so on) to disambiguate them so that it's easier to identify which one is failing.

* JUnitReport: fixed the reporting of failures as it wasn't consistent with "tests": the tests were the number of tests that we run but "failures" were the number of assertions that failed.
@jjmaestro
Copy link
Contributor Author

I've rebased the latest main and addressed the commit message rewording.

@bioball There's "one change requested" re. the output of the tests and I believe I've addressed it. Can you mark it as resolved so merging is not blocked?

Also, @bioball or @holzensp, can you unblock CI? The job is on hold, I believe the CI jobs don't run unless authorized? :-?

Thanks!

Copy link
Contributor

@bioball bioball left a comment

Choose a reason for hiding this comment

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

Great, thank you for the PR! These are definitely improvements.

FYI: I'm going to submit a follow-up PR to refine this a little bit more. But I think this is good enough to go in for now.

@bioball bioball merged commit 86d870b into apple:main Oct 24, 2024
5 checks passed
@jjmaestro
Copy link
Contributor Author

@bioball cool, thanks! 😄

@bioball
Copy link
Contributor

bioball commented Oct 24, 2024

Follow-up PR: #738

@jjmaestro
Copy link
Contributor Author

Follow-up PR: #738

I've commented in the new PR, IMHO there's a big change that removes a feature that I added in this PR. Let's discuss in there!

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.

3 participants