Merge pull request #47 from dorny/improvements-and-fixes

Improvements and fixes for v1
This commit is contained in:
Michal Dorner 2021-01-31 20:49:39 +01:00 committed by GitHub
commit 10ba3946cb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 56323 additions and 1413 deletions

View file

@ -49,9 +49,9 @@ jobs:
# Name of the Check Run which will be created # Name of the Check Run which will be created
name: '' name: ''
# Path to test report # Coma separated list of paths to test reports
# Supports wildcards via [fast-glob](https://github.com/mrmlnc/fast-glob) # Supports wildcards via [fast-glob](https://github.com/mrmlnc/fast-glob)
# Path may match multiple result files of same format # All matched result files must be of same format
path: '' path: ''
# Format of test report. Supported options: # Format of test report. Supported options:
@ -61,8 +61,20 @@ jobs:
# jest-junit # jest-junit
reporter: '' reporter: ''
# Enables code annotations with error message and stack trace captured during test execution # Limits which test suites are listed:
annotations: 'true' # all
# failed
list-suites: 'all'
# Limits which test cases are listed:
# all
# failed
# none
list-tests: 'all'
# Limits number of created annotations with error message and stack trace captured during test execution.
# Must be less or equal to 50.
max-annotations: '10'
# Set action as failed if test report contain any failed test # Set action as failed if test report contain any failed test
fail-on-error: 'true' fail-on-error: 'true'
@ -109,7 +121,7 @@ Or you can configure TRX test output in `*.csproj` or `Directory.Build.props`:
```xml ```xml
<PropertyGroup> <PropertyGroup>
<VSTestLogger>trx%3bLogFileName=$(MSBuildProjectName).trx</VSTestLogger> <VSTestLogger>trx%3bLogFileName=$(MSBuildProjectName).trx</VSTestLogger>
<VSTestResultsDirectory>$(MSBuildThisFileDirectory)/reports</VSTestResultsDirectory> <VSTestResultsDirectory>$(MSBuildThisFileDirectory)/TestResults/$(TargetFramework)</VSTestResultsDirectory>
</PropertyGroup> </PropertyGroup>
``` ```

View file

@ -1,38 +1,30 @@
![Tests failed](https://img.shields.io/badge/tests-1%20passed%2C%201%20skipped%2C%204%20failed-critical) ![Tests failed](https://img.shields.io/badge/tests-1%20passed%2C%204%20failed%2C%201%20skipped-critical)
## <a id="user-content-r0" href="#r0">fixtures/dart-json.json</a>
### fixtures/dart-json.json **6** tests were completed in **3.760s** with **1** passed, **4** failed and **1** skipped.
|Test suite|Passed|Failed|Skipped|Time|
**6** tests were completed in **3.760s** with **1** passed, **1** skipped and **4** failed. |:---|---:|---:|---:|---:|
|[test\main_test.dart](#r0s0)|1✔|3❌||74ms|
| Result | Suite | Tests | Time | Passed ✔️ | Skipped ✖️ | Failed ❌ | |[test\second_test.dart](#r0s1)||1❌|1✖|51ms|
| :---: | :--- | ---: | ---: | ---: | ---: | ---: | ### <a id="user-content-r0s0" href="#r0s0">test\main_test.dart</a>
| ❌ | [test\main_test.dart](#ts-0-test-maintest-dart) | 4 | 74ms | 1 | 0 | 3 | **4** tests were completed in **74ms** with **1** passed, **3** failed and **0** skipped.
| ❌ | [test\second_test.dart](#ts-1-test-secondtest-dart) | 2 | 51ms | 0 | 1 | 1 |
# Test Suites
## <a id="user-content-ts-0-test-maintest-dart" href="#ts-0-test-maintest-dart">test\main_test.dart</a>
### Test 1
**Test 1**
|Result|Test|Time| |Result|Test|Time|
|:---:|:---|---:| |:---:|:---|---:|
|✔️|Test 1 Passing test|36ms| |✔️|Test 1 Passing test|36ms|
### Test 1 Test 1.1 **Test 1 Test 1.1**
|Result|Test|Time| |Result|Test|Time|
|:---:|:---|---:| |:---:|:---|---:|
|❌|Test 1 Test 1.1 Failing test|20ms| |❌|Test 1 Test 1.1 Failing test|20ms|
|❌|Test 1 Test 1.1 Exception in target unit|6ms| |❌|Test 1 Test 1.1 Exception in target unit|6ms|
### Test 2 **Test 2**
|Result|Test|Time| |Result|Test|Time|
|:---:|:---|---:| |:---:|:---|---:|
|❌|Test 2 Exception in test|12ms| |❌|Test 2 Exception in test|12ms|
### <a id="user-content-r0s1" href="#r0s1">test\second_test.dart</a>
## <a id="user-content-ts-1-test-secondtest-dart" href="#ts-1-test-secondtest-dart">test\second_test.dart</a> **2** tests were completed in **51ms** with **0** passed, **1** failed and **1** skipped.
|Result|Test|Time| |Result|Test|Time|
|:---:|:---|---:| |:---:|:---|---:|

View file

@ -1,23 +1,18 @@
![Tests failed](https://img.shields.io/badge/tests-3%20passed%2C%201%20skipped%2C%203%20failed-critical) ![Tests failed](https://img.shields.io/badge/tests-3%20passed%2C%203%20failed%2C%201%20skipped-critical)
## <a id="user-content-r0" href="#r0">fixtures/dotnet-trx.trx</a>
### fixtures/dotnet-trx.trx **7** tests were completed in **1.061s** with **3** passed, **3** failed and **1** skipped.
|Test suite|Passed|Failed|Skipped|Time|
**7** tests were completed in **1.061s** with **3** passed, **1** skipped and **3** failed. |:---|---:|---:|---:|---:|
|[DotnetTests.XUnitTests.CalculatorTests](#r0s0)|3✔|3❌|1✖|110ms|
| Result | Suite | Tests | Time | Passed ✔️ | Skipped ✖️ | Failed ❌ | ### <a id="user-content-r0s0" href="#r0s0">DotnetTests.XUnitTests.CalculatorTests</a>
| :---: | :--- | ---: | ---: | ---: | ---: | ---: | **7** tests were completed in **110ms** with **3** passed, **3** failed and **1** skipped.
| ❌ | [DotnetTests.XUnitTests.CalculatorTests](#ts-0-DotnetTests-XUnitTests-CalculatorTests) | 7 | 109.5761ms | 3 | 1 | 3 |
# Test Suites
## <a id="user-content-ts-0-DotnetTests-XUnitTests-CalculatorTests" href="#ts-0-DotnetTests-XUnitTests-CalculatorTests">DotnetTests.XUnitTests.CalculatorTests</a>
|Result|Test|Time| |Result|Test|Time|
|:---:|:---|---:| |:---:|:---|---:|
| ❌ | Exception_In_TargetTest | 0.4975ms | |❌|Exception_In_TargetTest|0ms|
| ❌ | Exception_In_Test | 2.2728ms | |❌|Exception_In_Test|2ms|
| ❌ | Failing_Test | 3.2953ms | |❌|Failing_Test|3ms|
| ✔️ | Passing_Test | 0.1254ms | |✔️|Passing_Test|0ms|
| ✔️ | Passing_Test_With_Name | 0.103ms | |✔️|Passing_Test_With_Name|0ms|
|✖️|Skipped_Test|1ms| |✖️|Skipped_Test|1ms|
| ✔️ | Timeout_Test | 102.2821ms | |✔️|Timeout_Test|102ms|

View file

@ -0,0 +1,67 @@
![Tests passed successfully](https://img.shields.io/badge/tests-803%20passed%2C%201%20skipped-success)
## <a id="user-content-r0" href="#r0">fixtures/external/FluentValidation.Tests.trx</a> ✔️
**804** tests were completed in **4.480s** with **803** passed, **0** failed and **1** skipped.
|Test suite|Passed|Failed|Skipped|Time|
|:---|---:|---:|---:|---:|
|FluentValidation.Tests.AbstractValidatorTester|35✔|||12ms|
|FluentValidation.Tests.AccessorCacheTests|4✔||1✖|4ms|
|FluentValidation.Tests.AssemblyScannerTester|2✔|||2ms|
|FluentValidation.Tests.CascadingFailuresTester|38✔|||23ms|
|FluentValidation.Tests.ChainedValidationTester|13✔|||6ms|
|FluentValidation.Tests.ChainingValidatorsTester|3✔|||1ms|
|FluentValidation.Tests.ChildRulesTests|2✔|||7ms|
|FluentValidation.Tests.CollectionValidatorWithParentTests|16✔|||13ms|
|FluentValidation.Tests.ComplexValidationTester|17✔|||26ms|
|FluentValidation.Tests.ConditionTests|18✔|||9ms|
|FluentValidation.Tests.CreditCardValidatorTests|2✔|||2ms|
|FluentValidation.Tests.CustomFailureActionTester|3✔|||1ms|
|FluentValidation.Tests.CustomMessageFormatTester|6✔|||3ms|
|FluentValidation.Tests.CustomValidatorTester|10✔|||6ms|
|FluentValidation.Tests.DefaultValidatorExtensionTester|30✔|||38ms|
|FluentValidation.Tests.EmailValidatorTests|36✔|||18ms|
|FluentValidation.Tests.EmptyTester|9✔|||5ms|
|FluentValidation.Tests.EnumValidatorTests|12✔|||24ms|
|FluentValidation.Tests.EqualValidatorTests|10✔|||3ms|
|FluentValidation.Tests.ExactLengthValidatorTester|6✔|||2ms|
|FluentValidation.Tests.ExclusiveBetweenValidatorTests|19✔|||6ms|
|FluentValidation.Tests.ExtensionTester|4✔|||1ms|
|FluentValidation.Tests.ForEachRuleTests|34✔|||47ms|
|FluentValidation.Tests.GreaterThanOrEqualToValidatorTester|14✔|||5ms|
|FluentValidation.Tests.GreaterThanValidatorTester|13✔|||4ms|
|FluentValidation.Tests.InclusiveBetweenValidatorTests|18✔|||4ms|
|FluentValidation.Tests.InheritanceValidatorTest|11✔|||18ms|
|FluentValidation.Tests.InlineValidatorTester|1✔|||2ms|
|FluentValidation.Tests.LanguageManagerTests|21✔|||28ms|
|FluentValidation.Tests.LengthValidatorTests|16✔|||17ms|
|FluentValidation.Tests.LessThanOrEqualToValidatorTester|13✔|||4ms|
|FluentValidation.Tests.LessThanValidatorTester|16✔|||6ms|
|FluentValidation.Tests.LocalisedMessagesTester|6✔|||3ms|
|FluentValidation.Tests.LocalisedNameTester|2✔|||1ms|
|FluentValidation.Tests.MemberAccessorTests|9✔|||5ms|
|FluentValidation.Tests.MessageFormatterTests|10✔|||2ms|
|FluentValidation.Tests.ModelLevelValidatorTests|2✔|||1ms|
|FluentValidation.Tests.NameResolutionPluggabilityTester|3✔|||2ms|
|FluentValidation.Tests.NotEmptyTester|10✔|||7ms|
|FluentValidation.Tests.NotEqualValidatorTests|11✔|||7ms|
|FluentValidation.Tests.NotNullTester|5✔|||1ms|
|FluentValidation.Tests.NullTester|5✔|||2ms|
|FluentValidation.Tests.OnFailureTests|10✔|||8ms|
|FluentValidation.Tests.PredicateValidatorTester|5✔|||2ms|
|FluentValidation.Tests.PropertyChainTests|7✔|||1ms|
|FluentValidation.Tests.RegularExpressionValidatorTests|15✔|||6ms|
|FluentValidation.Tests.RuleBuilderTests|29✔|||96ms|
|FluentValidation.Tests.RuleDependencyTests|14✔|||2.511s|
|FluentValidation.Tests.RulesetTests|21✔|||14ms|
|FluentValidation.Tests.ScalePrecisionValidatorTests|6✔|||4ms|
|FluentValidation.Tests.SharedConditionTests|42✔|||42ms|
|FluentValidation.Tests.StandalonePropertyValidationTester|1✔|||0ms|
|FluentValidation.Tests.StringEnumValidatorTests|10✔|||5ms|
|FluentValidation.Tests.TrackingCollectionTests|3✔|||2ms|
|FluentValidation.Tests.TransformTests|4✔|||3ms|
|FluentValidation.Tests.UserSeverityTester|7✔|||3ms|
|FluentValidation.Tests.UserStateTester|4✔|||3ms|
|FluentValidation.Tests.ValidateAndThrowTester|14✔|||25ms|
|FluentValidation.Tests.ValidationResultTests|8✔|||8ms|
|FluentValidation.Tests.ValidatorDescriptorTester|5✔|||1ms|
|FluentValidation.Tests.ValidatorSelectorTests|10✔|||9ms|
|FluentValidation.Tests.ValidatorTesterTester|73✔|||74ms|

View file

@ -1,38 +1,30 @@
![Tests failed](https://img.shields.io/badge/tests-1%20passed%2C%201%20skipped%2C%204%20failed-critical) ![Tests failed](https://img.shields.io/badge/tests-1%20passed%2C%204%20failed%2C%201%20skipped-critical)
## <a id="user-content-r0" href="#r0">fixtures/jest-junit.xml</a>
### fixtures/jest-junit.xml **6** tests were completed in **1.360s** with **1** passed, **4** failed and **1** skipped.
|Test suite|Passed|Failed|Skipped|Time|
**6** tests were completed in **1.360s** with **1** passed, **1** skipped and **4** failed. |:---|---:|---:|---:|---:|
|[__tests__\main.test.js](#r0s0)|1✔|3❌||486ms|
| Result | Suite | Tests | Time | Passed ✔️ | Skipped ✖️ | Failed ❌ | |[__tests__\second.test.js](#r0s1)||1❌|1✖|82ms|
| :---: | :--- | ---: | ---: | ---: | ---: | ---: | ### <a id="user-content-r0s0" href="#r0s0">__tests__\main.test.js</a>
| ❌ | [__tests__\main.test.js](#ts-0-tests-main-test-js) | 4 | 486ms | 1 | 0 | 3 | **4** tests were completed in **486ms** with **1** passed, **3** failed and **0** skipped.
| ❌ | [__tests__\second.test.js](#ts-1-tests-second-test-js) | 2 | 82ms | 0 | 1 | 1 |
# Test Suites
## <a id="user-content-ts-0-tests-main-test-js" href="#ts-0-tests-main-test-js">__tests__\main.test.js</a>
### Test 1
**Test 1**
|Result|Test|Time| |Result|Test|Time|
|:---:|:---|---:| |:---:|:---|---:|
|✔️|Passing test|1ms| |✔️|Passing test|1ms|
### Test 1 Test 1.1 **Test 1 Test 1.1**
|Result|Test|Time| |Result|Test|Time|
|:---:|:---|---:| |:---:|:---|---:|
|❌|Failing test|2ms| |❌|Failing test|2ms|
|❌|Exception in target unit|0ms| |❌|Exception in target unit|0ms|
### Test 2 **Test 2**
|Result|Test|Time| |Result|Test|Time|
|:---:|:---|---:| |:---:|:---|---:|
|❌|Exception in test|0ms| |❌|Exception in test|0ms|
### <a id="user-content-r0s1" href="#r0s1">__tests__\second.test.js</a>
## <a id="user-content-ts-1-tests-second-test-js" href="#ts-1-tests-second-test-js">__tests__\second.test.js</a> **2** tests were completed in **82ms** with **0** passed, **1** failed and **1** skipped.
|Result|Test|Time| |Result|Test|Time|
|:---:|:---|---:| |:---:|:---|---:|

View file

@ -0,0 +1,434 @@
![Tests failed](https://img.shields.io/badge/tests-4207%20passed%2C%202%20failed%2C%2030%20skipped-critical)
## <a id="user-content-r0" href="#r0">fixtures/external/jest/jest-test-results.xml</a>
**4239** tests were completed in **165.872s** with **4207** passed, **2** failed and **30** skipped.
|Test suite|Passed|Failed|Skipped|Time|
|:---|---:|---:|---:|---:|
|e2e/__tests__/asyncAndCallback.test.ts|1✔|||746ms|
|e2e/__tests__/asyncRegenerator.test.ts|1✔|||4.127s|
|e2e/__tests__/autoClearMocks.test.ts|2✔|||1.681s|
|e2e/__tests__/autoResetMocks.test.ts|2✔|||1.666s|
|e2e/__tests__/autoRestoreMocks.test.ts|2✔|||1.797s|
|e2e/__tests__/babelPluginJestHoist.test.ts|1✔|||6.249s|
|e2e/__tests__/badSourceMap.test.ts|1✔|||858ms|
|e2e/__tests__/beforeAllFiltered.ts|1✔|||958ms|
|e2e/__tests__/beforeEachQueue.ts|1✔||1✖|55ms|
|e2e/__tests__/callDoneTwice.test.ts|1✔|||882ms|
|e2e/__tests__/chaiAssertionLibrary.ts|1✔|||1.902s|
|e2e/__tests__/circularInequality.test.ts|1✔|||1.451s|
|e2e/__tests__/circusConcurrentEach.test.ts|2✔|||1.591s|
|e2e/__tests__/circusDeclarationErrors.test.ts|1✔|||869ms|
|e2e/__tests__/clearCache.test.ts|2✔|||1.004s|
|e2e/__tests__/cliHandlesExactFilenames.test.ts|2✔|||1.230s|
|e2e/__tests__/compareDomNodes.test.ts|1✔|||1.407s|
|e2e/__tests__/config.test.ts|6✔|||3.945s|
|e2e/__tests__/console.test.ts|7✔|||8.072s|
|e2e/__tests__/consoleAfterTeardown.test.ts|1✔|||1.341s|
|e2e/__tests__/consoleLogOutputWhenRunInBand.test.ts|1✔|||793ms|
|e2e/__tests__/coverageHandlebars.test.ts|1✔|||1.873s|
|e2e/__tests__/coverageRemapping.test.ts|1✔|||12.701s|
|e2e/__tests__/coverageReport.test.ts|12✔|||22.264s|
|e2e/__tests__/coverageThreshold.test.ts|5✔|||4.868s|
|e2e/__tests__/coverageTransformInstrumented.test.ts|1✔|||5.029s|
|e2e/__tests__/coverageWithoutTransform.test.ts|1✔|||1.075s|
|e2e/__tests__/createProcessObject.test.ts|1✔|||908ms|
|e2e/__tests__/customInlineSnapshotMatchers.test.ts|1✔|||2.206s|
|e2e/__tests__/customMatcherStackTrace.test.ts|2✔|||1.539s|
|e2e/__tests__/customReporters.test.ts|9✔|||6.553s|
|e2e/__tests__/customResolver.test.ts|1✔|||826ms|
|e2e/__tests__/customTestSequencers.test.ts|3✔|||2.757s|
|e2e/__tests__/debug.test.ts|1✔|||899ms|
|e2e/__tests__/declarationErrors.test.ts|3✔|||2.389s|
|e2e/__tests__/dependencyClash.test.ts|1✔|||833ms|
|e2e/__tests__/detectOpenHandles.ts|8✔|||7.528s|
|e2e/__tests__/domDiffing.test.ts|1✔|||1.361s|
|e2e/__tests__/doneInHooks.test.ts|1✔|||855ms|
|e2e/__tests__/dynamicRequireDependencies.ts|1✔|||847ms|
|e2e/__tests__/each.test.ts|7✔|||4.721s|
|e2e/__tests__/emptyDescribeWithHooks.test.ts|4✔|||2.886s|
|e2e/__tests__/emptySuiteError.test.ts|1✔|||885ms|
|e2e/__tests__/env.test.ts|6✔|||5.221s|
|e2e/__tests__/environmentAfterTeardown.test.ts|1✔|||892ms|
|e2e/__tests__/errorOnDeprecated.test.ts|1✔||24✖|56ms|
|e2e/__tests__/esmConfigFile.test.ts|3✔|||526ms|
|e2e/__tests__/executeTestsOnceInMpr.ts|1✔|||976ms|
|e2e/__tests__/existentRoots.test.ts|4✔|||627ms|
|e2e/__tests__/expectAsyncMatcher.test.ts|2✔|||2.732s|
|e2e/__tests__/expectInVm.test.ts|1✔|||1.527s|
|e2e/__tests__/extraGlobals.test.ts|1✔|||1.011s|
|e2e/__tests__/failureDetailsProperty.test.ts|1✔|||907ms|
|e2e/__tests__/failures.test.ts|7✔|||10.353s|
|e2e/__tests__/fakePromises.test.ts|2✔|||1.716s|
|e2e/__tests__/fatalWorkerError.test.ts|1✔|||3.167s|
|e2e/__tests__/filter.test.ts|7✔|||5.422s|
|e2e/__tests__/findRelatedFiles.test.ts|5✔|||6.230s|
|e2e/__tests__/focusedTests.test.ts|1✔|||888ms|
|e2e/__tests__/forceExit.test.ts|1✔|||2.208s|
|e2e/__tests__/generatorMock.test.ts|1✔|||1.027s|
|e2e/__tests__/global-mutation.test.ts|1✔|||40ms|
|e2e/__tests__/global.test.ts|1✔|||31ms|
|e2e/__tests__/globals.test.ts|10✔|||7.505s|
|e2e/__tests__/globalSetup.test.ts|10✔|||13.926s|
|e2e/__tests__/globalTeardown.test.ts|7✔|||11.886s|
|e2e/__tests__/hasteMapMockChanged.test.ts|1✔|||379ms|
|e2e/__tests__/hasteMapSha1.test.ts|1✔|||298ms|
|e2e/__tests__/hasteMapSize.test.ts|2✔|||397ms|
|e2e/__tests__/importedGlobals.test.ts|1✔|||1.043s|
|e2e/__tests__/injectGlobals.test.ts|2✔|||1.860s|
|e2e/__tests__/jasmineAsync.test.ts|15✔|||28.291s|
|e2e/__tests__/jasmineAsyncWithPendingDuringTest.ts|1✔||1✖|72ms|
|e2e/__tests__/jest.config.js.test.ts|3✔|||2.134s|
|e2e/__tests__/jest.config.ts.test.ts|5✔|||14.322s|
|[e2e/__tests__/jestChangedFiles.test.ts](#r0s75)|9✔|1❌||9.045s|
|e2e/__tests__/jestEnvironmentJsdom.test.ts|1✔|||1.744s|
|e2e/__tests__/jestRequireActual.test.ts|1✔|||1.665s|
|e2e/__tests__/jestRequireMock.test.ts|1✔|||2.119s|
|e2e/__tests__/json.test.ts|2✔|||29ms|
|e2e/__tests__/jsonReporter.test.ts|2✔|||1.514s|
|e2e/__tests__/lifecycles.ts|1✔|||861ms|
|e2e/__tests__/listTests.test.ts|2✔|||945ms|
|e2e/__tests__/locationInResults.test.ts|2✔|||1.764s|
|e2e/__tests__/logHeapUsage.test.ts|1✔|||884ms|
|e2e/__tests__/mockNames.test.ts|8✔|||6.771s|
|e2e/__tests__/modernFakeTimers.test.ts|2✔|||1.680s|
|e2e/__tests__/moduleNameMapper.test.ts|5✔|||5.395s|
|e2e/__tests__/moduleParentNullInTest.ts|1✔|||886ms|
|e2e/__tests__/multiProjectRunner.test.ts|14✔|||16.360s|
|e2e/__tests__/nativeAsyncMock.test.ts|1✔|||55ms|
|e2e/__tests__/nativeEsm.test.ts|2✔||1✖|905ms|
|e2e/__tests__/nativeEsmTypescript.test.ts|1✔|||956ms|
|e2e/__tests__/nestedEventLoop.test.ts|1✔|||1.422s|
|e2e/__tests__/nestedTestDefinitions.test.ts|4✔|||4.641s|
|e2e/__tests__/nodePath.test.ts|1✔|||866ms|
|e2e/__tests__/noTestFound.test.ts|2✔|||1.063s|
|e2e/__tests__/noTestsFound.test.ts|5✔|||2.739s|
|[e2e/__tests__/onlyChanged.test.ts](#r0s98)|8✔|1❌||22.281s|
|e2e/__tests__/onlyFailuresNonWatch.test.ts|1✔|||2.893s|
|e2e/__tests__/overrideGlobals.test.ts|2✔|||2.046s|
|e2e/__tests__/pnp.test.ts|1✔|||2.715s|
|e2e/__tests__/presets.test.ts|2✔|||1.966s|
|e2e/__tests__/processExit.test.ts|1✔|||1.070s|
|e2e/__tests__/promiseReject.test.ts|1✔|||967ms|
|e2e/__tests__/regexCharInPath.test.ts|1✔|||962ms|
|e2e/__tests__/requireAfterTeardown.test.ts|1✔|||921ms|
|e2e/__tests__/requireMain.test.ts|1✔|||1.137s|
|e2e/__tests__/requireMainAfterCreateRequire.test.ts|1✔|||966ms|
|e2e/__tests__/requireMainIsolateModules.test.ts|1✔|||976ms|
|e2e/__tests__/requireMainResetModules.test.ts|2✔|||1.961s|
|e2e/__tests__/requireV8Module.test.ts|1✔|||30ms|
|e2e/__tests__/resetModules.test.ts|1✔|||926ms|
|e2e/__tests__/resolve.test.ts|1✔|||1.863s|
|e2e/__tests__/resolveGetPaths.test.ts|1✔|||1.155s|
|e2e/__tests__/resolveNodeModule.test.ts|1✔|||943ms|
|e2e/__tests__/resolveNoFileExtensions.test.ts|2✔|||1.263s|
|e2e/__tests__/resolveWithPaths.test.ts|1✔|||1.170s|
|e2e/__tests__/runProgrammatically.test.ts|2✔|||575ms|
|e2e/__tests__/runTestsByPath.test.ts|1✔|||1.999s|
|e2e/__tests__/runtimeInternalModuleRegistry.test.ts|1✔|||1.202s|
|e2e/__tests__/selectProjects.test.ts|18✔|||5.236s|
|e2e/__tests__/setImmediate.test.ts|1✔|||904ms|
|e2e/__tests__/setupFilesAfterEnvConfig.test.ts|2✔|||1.967s|
|e2e/__tests__/showConfig.test.ts|1✔|||195ms|
|e2e/__tests__/skipBeforeAfterAll.test.ts|1✔|||1.061s|
|e2e/__tests__/snapshot-unknown.test.ts|1✔|||838ms|
|e2e/__tests__/snapshot.test.ts|9✔|||13.899s|
|e2e/__tests__/snapshotMockFs.test.ts|1✔|||883ms|
|e2e/__tests__/snapshotResolver.test.ts|1✔|||823ms|
|e2e/__tests__/snapshotSerializers.test.ts|2✔|||2.065s|
|e2e/__tests__/stackTrace.test.ts|7✔|||4.725s|
|e2e/__tests__/stackTraceNoCaptureStackTrace.test.ts|1✔|||899ms|
|e2e/__tests__/stackTraceSourceMaps.test.ts|1✔|||2.185s|
|e2e/__tests__/stackTraceSourceMapsWithCoverage.test.ts|1✔|||2.444s|
|e2e/__tests__/supportsDashedArgs.ts|2✔|||968ms|
|e2e/__tests__/symbol.test.ts|1✔|||49ms|
|e2e/__tests__/testEnvironment.test.ts|1✔|||1.628s|
|e2e/__tests__/testEnvironmentAsync.test.ts|1✔|||1.493s|
|e2e/__tests__/testEnvironmentCircus.test.ts|1✔|||1.501s|
|e2e/__tests__/testEnvironmentCircusAsync.test.ts|1✔|||1.507s|
|e2e/__tests__/testFailureExitCode.test.ts|2✔|||4.476s|
|e2e/__tests__/testInRoot.test.ts|1✔|||1.009s|
|e2e/__tests__/testNamePattern.test.ts|1✔|||859ms|
|e2e/__tests__/testNamePatternSkipped.test.ts|1✔|||991ms|
|e2e/__tests__/testPathPatternReporterMessage.test.ts|1✔|||3.076s|
|e2e/__tests__/testResultsProcessor.test.ts|1✔|||910ms|
|e2e/__tests__/testRetries.test.ts|4✔|||3.277s|
|e2e/__tests__/testTodo.test.ts|5✔|||3.573s|
|e2e/__tests__/timeouts.test.ts|4✔|||4.029s|
|e2e/__tests__/timeoutsLegacy.test.ts|1✔||3✖|71ms|
|e2e/__tests__/timerResetMocks.test.ts|2✔|||1.878s|
|e2e/__tests__/timerUseRealTimers.test.ts|1✔|||1.018s|
|e2e/__tests__/toMatchInlineSnapshot.test.ts|12✔|||23.917s|
|e2e/__tests__/toMatchInlineSnapshotWithRetries.test.ts|3✔|||4.670s|
|e2e/__tests__/toMatchSnapshot.test.ts|9✔|||17.025s|
|e2e/__tests__/toMatchSnapshotWithRetries.test.ts|2✔|||4.435s|
|e2e/__tests__/toMatchSnapshotWithStringSerializer.test.ts|3✔|||3.544s|
|e2e/__tests__/toThrowErrorMatchingInlineSnapshot.test.ts|4✔|||3.562s|
|e2e/__tests__/toThrowErrorMatchingSnapshot.test.ts|5✔|||3.524s|
|e2e/__tests__/transform.test.ts|16✔|||26.740s|
|e2e/__tests__/transformLinkedModules.test.ts|1✔|||783ms|
|e2e/__tests__/typescriptCoverage.test.ts|1✔|||2.893s|
|e2e/__tests__/unexpectedToken.test.ts|3✔|||3.411s|
|e2e/__tests__/useStderr.test.ts|1✔|||1.352s|
|e2e/__tests__/v8Coverage.test.ts|2✔|||2.412s|
|e2e/__tests__/verbose.test.ts|1✔|||683ms|
|e2e/__tests__/version.test.ts|1✔|||138ms|
|e2e/__tests__/watchModeNoAccess.test.ts|1✔|||4.370s|
|e2e/__tests__/watchModeOnlyFailed.test.ts|1✔|||1.394s|
|e2e/__tests__/watchModePatterns.test.ts|2✔|||3.503s|
|e2e/__tests__/watchModeUpdateSnapshot.test.ts|1✔|||1.075s|
|e2e/__tests__/workerForceExit.test.ts|2✔|||4.751s|
|e2e/__tests__/wrongEnv.test.ts|5✔|||3.877s|
|e2e/custom-test-sequencer/a.test.js|1✔|||29ms|
|e2e/custom-test-sequencer/b.test.js|1✔|||21ms|
|e2e/custom-test-sequencer/c.test.js|1✔|||42ms|
|e2e/custom-test-sequencer/d.test.js|1✔|||21ms|
|e2e/custom-test-sequencer/e.test.js|1✔|||27ms|
|e2e/test-in-root/spec.js|1✔|||19ms|
|e2e/test-in-root/test.js|1✔|||37ms|
|e2e/timer-reset-mocks/after-reset-all-mocks/timerAndMock.test.js|2✔|||30ms|
|e2e/timer-reset-mocks/with-reset-mocks/timerWithMock.test.js|1✔|||34ms|
|e2e/v8-coverage/empty-sourcemap/test.ts|1✔|||31ms|
|examples/angular/app.component.spec.ts|3✔|||654ms|
|examples/angular/shared/data.service.spec.ts|2✔|||431ms|
|examples/angular/shared/sub.service.spec.ts|1✔|||109ms|
|examples/async/__tests__/user.test.js|8✔|||96ms|
|examples/automatic-mocks/__tests__/automock.test.js|2✔|||74ms|
|examples/automatic-mocks/__tests__/createMockFromModule.test.js|2✔|||115ms|
|examples/automatic-mocks/__tests__/disableAutomocking.test.js|1✔|||24ms|
|examples/enzyme/__tests__/CheckboxWithLabel-test.js|1✔|||434ms|
|examples/getting-started/sum.test.js|1✔|||78ms|
|examples/jquery/__tests__/display_user.test.js|1✔|||196ms|
|examples/jquery/__tests__/fetch_current_user.test.js|2✔|||196ms|
|examples/manual-mocks/__tests__/file_summarizer.test.js|1✔|||87ms|
|examples/manual-mocks/__tests__/lodashMocking.test.js|1✔|||109ms|
|examples/manual-mocks/__tests__/user.test.js|1✔|||41ms|
|examples/manual-mocks/__tests__/userMocked.test.js|1✔|||105ms|
|examples/module-mock/__tests__/full_mock.js|1✔|||60ms|
|examples/module-mock/__tests__/mock_per_test.js|2✔|||116ms|
|examples/module-mock/__tests__/partial_mock.js|1✔|||215ms|
|examples/mongodb/__test__/db.test.js|1✔|||236ms|
|examples/react-native/__tests__/intro.test.js|4✔|||8.559s|
|examples/react-testing-library/__tests__/CheckboxWithLabel-test.js|1✔|||469ms|
|examples/react/__tests__/CheckboxWithLabel-test.js|1✔|||256ms|
|examples/snapshot/__tests__/clock.react.test.js|1✔|||62ms|
|examples/snapshot/__tests__/link.react.test.js|4✔|||181ms|
|examples/timer/__tests__/infinite_timer_game.test.js|1✔|||94ms|
|examples/timer/__tests__/timer_game.test.js|3✔|||74ms|
|examples/typescript/__tests__/calc.test.ts|6✔|||276ms|
|examples/typescript/__tests__/CheckboxWithLabel-test.tsx|1✔|||227ms|
|examples/typescript/__tests__/sub-test.ts|1✔|||43ms|
|examples/typescript/__tests__/sum-test.ts|2✔|||69ms|
|examples/typescript/__tests__/sum.test.js|2✔|||100ms|
|packages/babel-jest/src/__tests__/index.ts|6✔|||371ms|
|packages/babel-plugin-jest-hoist/src/__tests__/hoistPlugin.test.ts|4✔|||347ms|
|packages/diff-sequences/src/__tests__/index.property.test.ts|7✔|||357ms|
|packages/diff-sequences/src/__tests__/index.test.ts|48✔|||195ms|
|packages/expect/src/__tests__/assertionCounts.test.ts|6✔|||60ms|
|packages/expect/src/__tests__/asymmetricMatchers.test.ts|38✔|||207ms|
|packages/expect/src/__tests__/extend.test.ts|10✔|||99ms|
|packages/expect/src/__tests__/isError.test.ts|4✔|||43ms|
|packages/expect/src/__tests__/matchers-toContain.property.test.ts|2✔|||236ms|
|packages/expect/src/__tests__/matchers-toContainEqual.property.test.ts|2✔|||287ms|
|packages/expect/src/__tests__/matchers-toEqual.property.test.ts|2✔|||1.062s|
|packages/expect/src/__tests__/matchers-toStrictEqual.property.test.ts|3✔|||394ms|
|packages/expect/src/__tests__/matchers.test.js|592✔|||862ms|
|packages/expect/src/__tests__/spyMatchers.test.ts|248✔|||395ms|
|packages/expect/src/__tests__/stacktrace.test.ts|3✔|||69ms|
|packages/expect/src/__tests__/symbolInObjects.test.ts|3✔|||33ms|
|packages/expect/src/__tests__/toEqual-dom.test.ts|12✔|||99ms|
|packages/expect/src/__tests__/toThrowMatchers.test.ts|98✔|||257ms|
|packages/expect/src/__tests__/utils.test.ts|41✔|||147ms|
|packages/jest-circus/src/__tests__/afterAll.test.ts|6✔|||5.755s|
|packages/jest-circus/src/__tests__/baseTest.test.ts|2✔|||2.902s|
|packages/jest-circus/src/__tests__/circusItTestError.test.ts|8✔|||300ms|
|packages/jest-circus/src/__tests__/circusItTodoTestError.test.ts|3✔|||81ms|
|packages/jest-circus/src/__tests__/hooks.test.ts|3✔|||3.762s|
|packages/jest-circus/src/__tests__/hooksError.test.ts|32✔|||127ms|
|packages/jest-cli/src/__tests__/cli/args.test.ts|17✔|||345ms|
|packages/jest-cli/src/init/__tests__/init.test.js|24✔|||119ms|
|packages/jest-cli/src/init/__tests__/modifyPackageJson.test.ts|4✔|||30ms|
|packages/jest-config/src/__tests__/Defaults.test.ts|1✔|||672ms|
|packages/jest-config/src/__tests__/getMaxWorkers.test.ts|7✔|||67ms|
|packages/jest-config/src/__tests__/normalize.test.js|118✔|||798ms|
|packages/jest-config/src/__tests__/readConfig.test.ts|1✔|||76ms|
|packages/jest-config/src/__tests__/readConfigs.test.ts|3✔|||135ms|
|packages/jest-config/src/__tests__/resolveConfigPath.test.ts|10✔|||183ms|
|packages/jest-config/src/__tests__/setFromArgv.test.ts|4✔|||53ms|
|packages/jest-config/src/__tests__/validatePattern.test.ts|4✔|||52ms|
|packages/jest-console/src/__tests__/bufferedConsole.test.ts|20✔|||171ms|
|packages/jest-console/src/__tests__/CustomConsole.test.ts|23✔|||115ms|
|packages/jest-console/src/__tests__/getConsoleOutput.test.ts|12✔|||56ms|
|packages/jest-core/src/__tests__/FailedTestsCache.test.js|1✔|||25ms|
|packages/jest-core/src/__tests__/getNoTestsFoundMessage.test.js|5✔|||61ms|
|packages/jest-core/src/__tests__/globals.test.ts|1✔|||22ms|
|packages/jest-core/src/__tests__/runJest.test.js|2✔|||261ms|
|packages/jest-core/src/__tests__/SearchSource.test.ts|27✔|||2.596s|
|packages/jest-core/src/__tests__/SnapshotInteractiveMode.test.js|13✔|||89ms|
|packages/jest-core/src/__tests__/TestScheduler.test.js|8✔|||520ms|
|packages/jest-core/src/__tests__/testSchedulerHelper.test.js|12✔|||48ms|
|packages/jest-core/src/__tests__/watch.test.js|80✔|||6.755s|
|packages/jest-core/src/__tests__/watchFileChanges.test.ts|1✔|||1.514s|
|packages/jest-core/src/__tests__/watchFilenamePatternMode.test.js|2✔|||165ms|
|packages/jest-core/src/__tests__/watchTestNamePatternMode.test.js|1✔|||246ms|
|packages/jest-core/src/lib/__tests__/isValidPath.test.ts|3✔|||166ms|
|packages/jest-core/src/lib/__tests__/logDebugMessages.test.ts|3✔|||48ms|
|packages/jest-create-cache-key-function/src/__tests__/index.test.ts|1✔|||75ms|
|packages/jest-diff/src/__tests__/diff.test.ts|107✔|||625ms|
|packages/jest-diff/src/__tests__/diffStringsRaw.test.ts|2✔|||55ms|
|packages/jest-diff/src/__tests__/getAlignedDiffs.test.ts|24✔|||72ms|
|packages/jest-diff/src/__tests__/joinAlignedDiffs.test.ts|6✔|||44ms|
|packages/jest-docblock/src/__tests__/index.test.ts|36✔|||177ms|
|packages/jest-each/src/__tests__/array.test.ts|159✔|||192ms|
|packages/jest-each/src/__tests__/index.test.ts|10✔|||44ms|
|packages/jest-each/src/__tests__/template.test.ts|242✔|||483ms|
|packages/jest-environment-jsdom/src/__tests__/jsdom_environment.test.ts|2✔|||783ms|
|packages/jest-environment-node/src/__tests__/node_environment.test.ts|6✔|||184ms|
|packages/jest-fake-timers/src/__tests__/legacyFakeTimers.test.ts|50✔|||302ms|
|packages/jest-fake-timers/src/__tests__/modernFakeTimers.test.ts|40✔|||317ms|
|packages/jest-get-type/src/__tests__/getType.test.ts|14✔|||45ms|
|packages/jest-get-type/src/__tests__/isPrimitive.test.ts|18✔|||36ms|
|packages/jest-globals/src/__tests__/index.ts|1✔|||533ms|
|packages/jest-haste-map/src/__tests__/get_mock_name.test.js|1✔|||22ms|
|packages/jest-haste-map/src/__tests__/includes_dotfiles.test.ts|1✔|||337ms|
|packages/jest-haste-map/src/__tests__/index.test.js|44✔|||1.145s|
|packages/jest-haste-map/src/__tests__/worker.test.js|7✔|||100ms|
|packages/jest-haste-map/src/crawlers/__tests__/node.test.js|10✔|||170ms|
|packages/jest-haste-map/src/crawlers/__tests__/watchman.test.js|8✔|||153ms|
|packages/jest-haste-map/src/lib/__tests__/dependencyExtractor.test.js|15✔|||56ms|
|packages/jest-haste-map/src/lib/__tests__/fast_path.test.js|5✔|||29ms|
|packages/jest-haste-map/src/lib/__tests__/getPlatformExtension.test.js|1✔|||35ms|
|packages/jest-haste-map/src/lib/__tests__/isRegExpSupported.test.js|2✔|||31ms|
|packages/jest-haste-map/src/lib/__tests__/normalizePathSep.test.js|2✔|||35ms|
|packages/jest-jasmine2/src/__tests__/concurrent.test.ts|3✔|||24ms|
|packages/jest-jasmine2/src/__tests__/expectationResultFactory.test.ts|7✔|||70ms|
|packages/jest-jasmine2/src/__tests__/hooksError.test.ts|32✔|||51ms|
|packages/jest-jasmine2/src/__tests__/iterators.test.ts|4✔|||43ms|
|packages/jest-jasmine2/src/__tests__/itTestError.test.ts|6✔|||32ms|
|packages/jest-jasmine2/src/__tests__/itToTestAlias.test.ts|1✔|||23ms|
|packages/jest-jasmine2/src/__tests__/pTimeout.test.ts|3✔|||44ms|
|packages/jest-jasmine2/src/__tests__/queueRunner.test.ts|6✔|||93ms|
|packages/jest-jasmine2/src/__tests__/reporter.test.ts|1✔|||107ms|
|packages/jest-jasmine2/src/__tests__/Suite.test.ts|1✔|||84ms|
|packages/jest-jasmine2/src/__tests__/todoError.test.ts|3✔|||27ms|
|packages/jest-leak-detector/src/__tests__/index.test.ts|6✔|||986ms|
|packages/jest-matcher-utils/src/__tests__/deepCyclicCopyReplaceable.test.ts|11✔|||49ms|
|packages/jest-matcher-utils/src/__tests__/deepCyclicCopyReplaceableDom.test.ts|2✔|||48ms|
|packages/jest-matcher-utils/src/__tests__/index.test.ts|48✔|||391ms|
|packages/jest-matcher-utils/src/__tests__/printDiffOrStringify.test.ts|21✔|||114ms|
|packages/jest-matcher-utils/src/__tests__/Replaceable.test.ts|17✔|||111ms|
|packages/jest-message-util/src/__tests__/messages.test.ts|11✔|||205ms|
|packages/jest-mock/src/__tests__/index.test.ts|84✔|||509ms|
|packages/jest-regex-util/src/__tests__/index.test.ts|8✔|||56ms|
|packages/jest-repl/src/__tests__/jest_repl.test.js|1✔|||1.172s|
|packages/jest-repl/src/__tests__/runtime_cli.test.js|4✔|||4.094s|
|packages/jest-reporters/src/__tests__/CoverageReporter.test.js|12✔|||397ms|
|packages/jest-reporters/src/__tests__/CoverageWorker.test.js|2✔|||199ms|
|packages/jest-reporters/src/__tests__/DefaultReporter.test.js|2✔|||148ms|
|packages/jest-reporters/src/__tests__/generateEmptyCoverage.test.js|3✔|||1.129s|
|packages/jest-reporters/src/__tests__/getResultHeader.test.js|4✔|||30ms|
|packages/jest-reporters/src/__tests__/getSnapshotStatus.test.js|3✔|||28ms|
|packages/jest-reporters/src/__tests__/getSnapshotSummary.test.js|4✔|||49ms|
|packages/jest-reporters/src/__tests__/getWatermarks.test.ts|2✔|||37ms|
|packages/jest-reporters/src/__tests__/NotifyReporter.test.ts|18✔|||166ms|
|packages/jest-reporters/src/__tests__/SummaryReporter.test.js|4✔|||366ms|
|packages/jest-reporters/src/__tests__/utils.test.ts|10✔|||85ms|
|packages/jest-reporters/src/__tests__/VerboseReporter.test.js|11✔|||425ms|
|packages/jest-resolve-dependencies/src/__tests__/dependency_resolver.test.ts|11✔|||666ms|
|packages/jest-resolve/src/__tests__/isBuiltinModule.test.ts|4✔|||36ms|
|packages/jest-resolve/src/__tests__/resolve.test.ts|16✔|||1.308s|
|packages/jest-runner/src/__tests__/testRunner.test.ts|2✔|||905ms|
|packages/jest-runtime/src/__tests__/instrumentation.test.ts|1✔|||275ms|
|packages/jest-runtime/src/__tests__/runtime_create_mock_from_module.test.js|3✔|||606ms|
|packages/jest-runtime/src/__tests__/runtime_environment.test.js|2✔|||497ms|
|packages/jest-runtime/src/__tests__/runtime_internal_module.test.js|4✔|||727ms|
|packages/jest-runtime/src/__tests__/runtime_jest_fn.js|4✔|||479ms|
|packages/jest-runtime/src/__tests__/runtime_jest_spy_on.test.js|2✔|||521ms|
|packages/jest-runtime/src/__tests__/runtime_mock.test.js|4✔|||743ms|
|packages/jest-runtime/src/__tests__/runtime_module_directories.test.js|4✔|||525ms|
|packages/jest-runtime/src/__tests__/runtime_node_path.test.js|4✔|||1.088s|
|packages/jest-runtime/src/__tests__/runtime_require_actual.test.js|2✔|||478ms|
|packages/jest-runtime/src/__tests__/runtime_require_cache.test.js|2✔|||454ms|
|packages/jest-runtime/src/__tests__/runtime_require_mock.test.js|13✔|||962ms|
|packages/jest-runtime/src/__tests__/runtime_require_module_no_ext.test.js|1✔|||261ms|
|packages/jest-runtime/src/__tests__/runtime_require_module_or_mock_transitive_deps.test.js|6✔|||2.366s|
|packages/jest-runtime/src/__tests__/runtime_require_module_or_mock.test.js|17✔|||1.223s|
|packages/jest-runtime/src/__tests__/runtime_require_module.test.js|27✔|||2.439s|
|packages/jest-runtime/src/__tests__/runtime_require_resolve.test.ts|5✔|||707ms|
|packages/jest-runtime/src/__tests__/runtime_wrap.js|2✔|||263ms|
|packages/jest-runtime/src/__tests__/Runtime-sourceMaps.test.js|1✔|||584ms|
|packages/jest-runtime/src/__tests__/Runtime-statics.test.js|2✔|||162ms|
|packages/jest-serializer/src/__tests__/index.test.ts|17✔|||158ms|
|packages/jest-snapshot/src/__tests__/dedentLines.test.ts|17✔|||94ms|
|packages/jest-snapshot/src/__tests__/InlineSnapshots.test.ts|22✔|||1.149s|
|packages/jest-snapshot/src/__tests__/matcher.test.ts|1✔|||131ms|
|packages/jest-snapshot/src/__tests__/mockSerializer.test.ts|10✔|||45ms|
|packages/jest-snapshot/src/__tests__/printSnapshot.test.ts|71✔|||1.188s|
|packages/jest-snapshot/src/__tests__/SnapshotResolver.test.ts|10✔|||98ms|
|packages/jest-snapshot/src/__tests__/throwMatcher.test.ts|3✔|||481ms|
|packages/jest-snapshot/src/__tests__/utils.test.ts|26✔|||214ms|
|packages/jest-source-map/src/__tests__/getCallsite.test.ts|3✔|||86ms|
|packages/jest-test-result/src/__tests__/formatTestResults.test.ts|1✔|||53ms|
|packages/jest-test-sequencer/src/__tests__/test_sequencer.test.js|8✔|||251ms|
|packages/jest-transform/src/__tests__/ScriptTransformer.test.ts|22✔|||1.660s|
|packages/jest-transform/src/__tests__/shouldInstrument.test.ts|25✔|||155ms|
|packages/jest-util/src/__tests__/createProcessObject.test.ts|4✔|||81ms|
|packages/jest-util/src/__tests__/deepCyclicCopy.test.ts|12✔|||86ms|
|packages/jest-util/src/__tests__/errorWithStack.test.ts|1✔|||41ms|
|packages/jest-util/src/__tests__/formatTime.test.ts|11✔|||82ms|
|packages/jest-util/src/__tests__/globsToMatcher.test.ts|4✔|||56ms|
|packages/jest-util/src/__tests__/installCommonGlobals.test.ts|2✔|||68ms|
|packages/jest-util/src/__tests__/isInteractive.test.ts|2✔|||35ms|
|packages/jest-util/src/__tests__/isPromise.test.ts|10✔|||30ms|
|packages/jest-validate/src/__tests__/validate.test.ts|23✔|||283ms|
|packages/jest-validate/src/__tests__/validateCLIOptions.test.js|6✔|||83ms|
|packages/jest-watcher/src/lib/__tests__/formatTestNameByPattern.test.ts|11✔|||129ms|
|packages/jest-watcher/src/lib/__tests__/prompt.test.ts|3✔|||91ms|
|packages/jest-watcher/src/lib/__tests__/scroll.test.ts|5✔|||57ms|
|packages/jest-worker/src/__tests__/Farm.test.js|10✔|||158ms|
|packages/jest-worker/src/__tests__/FifoQueue.test.js|3✔|||48ms|
|packages/jest-worker/src/__tests__/index.test.js|8✔|||230ms|
|packages/jest-worker/src/__tests__/PriorityQueue.test.js|5✔|||63ms|
|packages/jest-worker/src/__tests__/process-integration.test.js|5✔|||62ms|
|packages/jest-worker/src/__tests__/thread-integration.test.js|6✔|||114ms|
|packages/jest-worker/src/__tests__/WorkerPool.test.js|3✔|||51ms|
|packages/jest-worker/src/base/__tests__/BaseWorkerPool.test.js|11✔|||653ms|
|packages/jest-worker/src/workers/__tests__/ChildProcessWorker.test.js|17✔|||184ms|
|packages/jest-worker/src/workers/__tests__/NodeThreadsWorker.test.js|15✔|||258ms|
|packages/jest-worker/src/workers/__tests__/processChild.test.js|10✔|||135ms|
|packages/jest-worker/src/workers/__tests__/threadChild.test.js|10✔|||120ms|
|packages/pretty-format/src/__tests__/AsymmetricMatcher.test.ts|38✔|||137ms|
|packages/pretty-format/src/__tests__/ConvertAnsi.test.ts|6✔|||43ms|
|packages/pretty-format/src/__tests__/DOMCollection.test.ts|10✔|||64ms|
|packages/pretty-format/src/__tests__/DOMElement.test.ts|28✔|||148ms|
|packages/pretty-format/src/__tests__/Immutable.test.ts|111✔|||443ms|
|packages/pretty-format/src/__tests__/prettyFormat.test.ts|86✔|||219ms|
|packages/pretty-format/src/__tests__/react.test.tsx|55✔|||325ms|
|packages/pretty-format/src/__tests__/ReactElement.test.ts|3✔|||64ms|
### <a id="user-content-r0s75" href="#r0s75">e2e/__tests__/jestChangedFiles.test.ts</a>
**10** tests were completed in **9.045s** with **9** passed, **1** failed and **0** skipped.
|Result|Test|Time|
|:---:|:---|---:|
|✔️|gets hg SCM roots and dedupes them|559ms|
|✔️|gets git SCM roots and dedupes them|416ms|
|✔️|gets mixed git and hg SCM roots and dedupes them|467ms|
|✔️|gets changed files for git|2.298s|
|✔️|monitors only root paths for git|151ms|
|✔️|does not find changes in files with no diff, for git|628ms|
|✔️|handles a bad revision for "changedSince", for git|878ms|
|❌|gets changed files for hg|2.219s|
|✔️|monitors only root paths for hg|281ms|
|✔️|handles a bad revision for "changedSince", for hg|949ms|
### <a id="user-content-r0s98" href="#r0s98">e2e/__tests__/onlyChanged.test.ts</a>
**9** tests were completed in **22.281s** with **8** passed, **1** failed and **0** skipped.
|Result|Test|Time|
|:---:|:---|---:|
|✔️|run for "onlyChanged" and "changedSince"|1.464s|
|✔️|run only changed files|5.196s|
|✔️|report test coverage for only changed files|1.889s|
|✔️|report test coverage of source on test file change under only changed files|822ms|
|✔️|do not pickup non-tested files when reporting coverage on only changed files|861ms|
|✔️|collect test coverage when using onlyChanged|1.058s|
|✔️|onlyChanged in config is overwritten by --all or testPathPattern|7.023s|
|❌|gets changed files for hg|3.765s|
|✔️|path on Windows is case-insensitive|0ms|

View file

@ -1,98 +1,106 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`dart-json tests matches report snapshot 1`] = ` exports[`dart-json tests matches report snapshot 1`] = `
Object { TestRunResult {
"annotations": Array [ "path": "fixtures/dart-json.json",
Object { "suites": Array [
"annotation_level": "failure", TestSuiteResult {
"end_line": 13, "groups": Array [
"message": "Expected: <2> TestGroupResult {
Actual: <1> "name": "Test 1",
"tests": Array [
TestCaseResult {
package:test_api expect "error": undefined,
test\\\\main_test.dart 13:9 main.<fn>.<fn>.<fn> "name": "Test 1 Passing test",
", "result": "success",
"path": "test/main_test.dart", "time": 36,
"start_line": 13,
"title": "[test\\\\main_test.dart] Test 1 Test 1.1 Failing test",
},
Object {
"annotation_level": "failure",
"end_line": 2,
"message": "Exception: Some error
package:darttest/main.dart 2:3 throwError
test\\\\main_test.dart 17:9 main.<fn>.<fn>.<fn>
",
"path": "lib/main.dart",
"start_line": 2,
"title": "[test\\\\main_test.dart] Test 1 Test 1.1 Exception in target unit",
},
Object {
"annotation_level": "failure",
"end_line": 24,
"message": "Exception: Some error
test\\\\main_test.dart 24:7 main.<fn>.<fn>
",
"path": "test/main_test.dart",
"start_line": 24,
"title": "[test\\\\main_test.dart] Test 2 Exception in test",
},
Object {
"annotation_level": "failure",
"end_line": 5,
"message": "TimeoutException after 0:00:00.000001: Test timed out after 0 seconds.
dart:isolate _RawReceivePortImpl._handleMessage
",
"path": "test/second_test.dart",
"start_line": 5,
"title": "[test\\\\second_test.dart] Timeout test",
}, },
], ],
"summary": "![Tests failed](https://img.shields.io/badge/tests-1%20passed%2C%201%20skipped%2C%204%20failed-critical) },
TestGroupResult {
### fixtures/dart-json.json "name": "Test 1 Test 1.1",
"tests": Array [
**6** tests were completed in **3.760s** with **1** passed, **1** skipped and **4** failed. TestCaseResult {
"error": Object {
| Result | Suite | Tests | Time | Passed ✔️ | Skipped ✖️ | Failed ❌ | "line": 13,
| :---: | :--- | ---: | ---: | ---: | ---: | ---: | "message": "Expected: <2>
| ❌ | [test\\\\main_test.dart](#ts-0-test-maintest-dart) | 4 | 74ms | 1 | 0 | 3 | Actual: <1>
| ❌ | [test\\\\second_test.dart](#ts-1-test-secondtest-dart) | 2 | 51ms | 0 | 1 | 1 |
# Test Suites
## <a id=\\"user-content-ts-0-test-maintest-dart\\" href=\\"#ts-0-test-maintest-dart\\">test\\\\main_test.dart</a> ❌
### Test 1
| Result | Test | Time |
| :---: | :--- | ---: |
| ✔️ | Test 1 Passing test | 36ms |
### Test 1 Test 1.1
| Result | Test | Time |
| :---: | :--- | ---: |
| ❌ | Test 1 Test 1.1 Failing test | 20ms |
| ❌ | Test 1 Test 1.1 Exception in target unit | 6ms |
### Test 2
| Result | Test | Time |
| :---: | :--- | ---: |
| ❌ | Test 2 Exception in test | 12ms |
## <a id=\\"user-content-ts-1-test-secondtest-dart\\" href=\\"#ts-1-test-secondtest-dart\\">test\\\\second_test.dart</a> ❌
| Result | Test | Time |
| :---: | :--- | ---: |
| ❌ | Timeout test | 37ms |
| ✖️ | Skipped test | 14ms |
", ",
"title": "Dart tests ❌", "path": "test/main_test.dart",
"stackTrace": "package:test_api expect
test\\\\main_test.dart 13:9 main.<fn>.<fn>.<fn>
",
},
"name": "Test 1 Test 1.1 Failing test",
"result": "failed",
"time": 20,
},
TestCaseResult {
"error": Object {
"line": 2,
"message": "Exception: Some error",
"path": "lib/main.dart",
"stackTrace": "package:darttest/main.dart 2:3 throwError
test\\\\main_test.dart 17:9 main.<fn>.<fn>.<fn>
",
},
"name": "Test 1 Test 1.1 Exception in target unit",
"result": "failed",
"time": 6,
},
],
},
TestGroupResult {
"name": "Test 2",
"tests": Array [
TestCaseResult {
"error": Object {
"line": 24,
"message": "Exception: Some error",
"path": "test/main_test.dart",
"stackTrace": "test\\\\main_test.dart 24:7 main.<fn>.<fn>
",
},
"name": "Test 2 Exception in test",
"result": "failed",
"time": 12,
},
],
},
],
"name": "test\\\\main_test.dart",
"totalTime": undefined,
},
TestSuiteResult {
"groups": Array [
TestGroupResult {
"name": null,
"tests": Array [
TestCaseResult {
"error": Object {
"line": 5,
"message": "TimeoutException after 0:00:00.000001: Test timed out after 0 seconds.",
"path": "test/second_test.dart",
"stackTrace": "dart:isolate _RawReceivePortImpl._handleMessage
",
},
"name": "Timeout test",
"result": "failed",
"time": 37,
},
TestCaseResult {
"error": undefined,
"name": "Skipped test",
"result": "skipped",
"time": 14,
},
],
},
],
"name": "test\\\\second_test.dart",
"totalTime": undefined,
},
],
"totalTime": 3760,
} }
`; `;

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,31 +1,30 @@
import * as fs from 'fs' import * as fs from 'fs'
import * as path from 'path' import * as path from 'path'
import {parseDartJson} from '../src/parsers/dart-json/dart-json-parser' import {DartJsonParser} from '../src/parsers/dart-json/dart-json-parser'
import {ParseOptions} from '../src/parsers/parser-types' import {ParseOptions} from '../src/test-parser'
import {getReport} from '../src/report/get-report'
import {normalizeFilePath} from '../src/utils/file-utils' import {normalizeFilePath} from '../src/utils/file-utils'
const fixturePath = path.join(__dirname, 'fixtures', 'dart-json.json')
const outputPath = path.join(__dirname, '__outputs__', 'dart-json.md')
const xmlFixture = {
path: normalizeFilePath(path.relative(__dirname, fixturePath)),
content: fs.readFileSync(fixturePath, {encoding: 'utf8'})
}
describe('dart-json tests', () => { describe('dart-json tests', () => {
it('matches report snapshot', async () => { it('matches report snapshot', async () => {
const opts: ParseOptions = { const opts: ParseOptions = {
name: 'Dart tests', parseErrors: true,
annotations: true,
trackedFiles: ['lib/main.dart', 'test/main_test.dart', 'test/second_test.dart'], trackedFiles: ['lib/main.dart', 'test/main_test.dart', 'test/second_test.dart'],
workDir: 'C:/Users/Michal/Workspace/dorny/test-check/reports/dart/' workDir: 'C:/Users/Michal/Workspace/dorny/test-check/reports/dart/'
} }
const result = await parseDartJson([xmlFixture], opts) const fixturePath = path.join(__dirname, 'fixtures', 'dart-json.json')
fs.mkdirSync(path.dirname(outputPath), {recursive: true}) const outputPath = path.join(__dirname, '__outputs__', 'dart-json.md')
fs.writeFileSync(outputPath, result?.output?.summary ?? '') const filePath = normalizeFilePath(path.relative(__dirname, fixturePath))
const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'})
expect(result.success).toBeFalsy() const parser = new DartJsonParser(opts)
expect(result?.output).toMatchSnapshot() const result = await parser.parse(filePath, fileContent)
expect(result).toMatchSnapshot()
const report = getReport([result])
fs.mkdirSync(path.dirname(outputPath), {recursive: true})
fs.writeFileSync(outputPath, report)
}) })
}) })

View file

@ -1,31 +1,51 @@
import * as fs from 'fs' import * as fs from 'fs'
import * as path from 'path' import * as path from 'path'
import {parseDotnetTrx} from '../src/parsers/dotnet-trx/dotnet-trx-parser' import {DotnetTrxParser} from '../src/parsers/dotnet-trx/dotnet-trx-parser'
import {ParseOptions} from '../src/parsers/parser-types' import {ParseOptions} from '../src/test-parser'
import {getReport} from '../src/report/get-report'
import {normalizeFilePath} from '../src/utils/file-utils' import {normalizeFilePath} from '../src/utils/file-utils'
const fixturePath = path.join(__dirname, 'fixtures', 'dotnet-trx.trx')
const outputPath = path.join(__dirname, '__outputs__', 'dotnet-trx.md')
const xmlFixture = {
path: normalizeFilePath(path.relative(__dirname, fixturePath)),
content: fs.readFileSync(fixturePath, {encoding: 'utf8'})
}
describe('dotnet-trx tests', () => { describe('dotnet-trx tests', () => {
it('matches report snapshot', async () => { it('matches report snapshot', async () => {
const fixturePath = path.join(__dirname, 'fixtures', 'dotnet-trx.trx')
const outputPath = path.join(__dirname, '__outputs__', 'dotnet-trx.md')
const filePath = normalizeFilePath(path.relative(__dirname, fixturePath))
const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'})
const opts: ParseOptions = { const opts: ParseOptions = {
name: 'Dotnet TRX tests', parseErrors: true,
annotations: true,
trackedFiles: ['DotnetTests.Unit/Calculator.cs', 'DotnetTests.XUnitTests/CalculatorTests.cs'], trackedFiles: ['DotnetTests.Unit/Calculator.cs', 'DotnetTests.XUnitTests/CalculatorTests.cs'],
workDir: 'C:/Users/Michal/Workspace/dorny/test-check/reports/dotnet/' workDir: 'C:/Users/Michal/Workspace/dorny/test-check/reports/dotnet/'
} }
const result = await parseDotnetTrx([xmlFixture], opts) const parser = new DotnetTrxParser(opts)
fs.mkdirSync(path.dirname(outputPath), {recursive: true}) const result = await parser.parse(filePath, fileContent)
fs.writeFileSync(outputPath, result?.output?.summary ?? '') expect(result).toMatchSnapshot()
expect(result.success).toBeFalsy() const report = getReport([result])
expect(result?.output).toMatchSnapshot() fs.mkdirSync(path.dirname(outputPath), {recursive: true})
fs.writeFileSync(outputPath, report)
})
it('report from FluentValidation test results matches snapshot', async () => {
const fixturePath = path.join(__dirname, 'fixtures', 'external', 'FluentValidation.Tests.trx')
const outputPath = path.join(__dirname, '__outputs__', 'fluent-validation-test-results.md')
const filePath = normalizeFilePath(path.relative(__dirname, fixturePath))
const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'})
const opts: ParseOptions = {
trackedFiles: [],
parseErrors: true,
workDir: ''
}
const parser = new DotnetTrxParser(opts)
const result = await parser.parse(filePath, fileContent)
expect(result).toMatchSnapshot()
const report = getReport([result], {listTests: 'failed'})
fs.mkdirSync(path.dirname(outputPath), {recursive: true})
fs.writeFileSync(outputPath, report)
}) })
}) })

File diff suppressed because it is too large Load diff

2317
__tests__/fixtures/external/jest/files.txt vendored Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,31 +1,53 @@
import * as fs from 'fs' import * as fs from 'fs'
import * as path from 'path' import * as path from 'path'
import {parseJestJunit} from '../src/parsers/jest-junit/jest-junit-parser' import {JestJunitParser} from '../src/parsers/jest-junit/jest-junit-parser'
import {ParseOptions} from '../src/parsers/parser-types' import {ParseOptions} from '../src/test-parser'
import {getReport} from '../src/report/get-report'
import {normalizeFilePath} from '../src/utils/file-utils' import {normalizeFilePath} from '../src/utils/file-utils'
describe('jest-junit tests', () => {
it('report from ./reports/jest test results matches snapshot', async () => {
const fixturePath = path.join(__dirname, 'fixtures', 'jest-junit.xml') const fixturePath = path.join(__dirname, 'fixtures', 'jest-junit.xml')
const outputPath = path.join(__dirname, '__outputs__', 'jest-junit.md') const outputPath = path.join(__dirname, '__outputs__', 'jest-junit.md')
const xmlFixture = { const filePath = normalizeFilePath(path.relative(__dirname, fixturePath))
path: normalizeFilePath(path.relative(__dirname, fixturePath)), const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'})
content: fs.readFileSync(fixturePath, {encoding: 'utf8'})
}
describe('jest-junit tests', () => {
it('matches report snapshot', async () => {
const opts: ParseOptions = { const opts: ParseOptions = {
name: 'jest tests', parseErrors: true,
annotations: true,
trackedFiles: ['__tests__/main.test.js', '__tests__/second.test.js', 'lib/main.js'], trackedFiles: ['__tests__/main.test.js', '__tests__/second.test.js', 'lib/main.js'],
workDir: 'C:/Users/Michal/Workspace/dorny/test-check/reports/jest/' workDir: 'C:/Users/Michal/Workspace/dorny/test-check/reports/jest/'
} }
const result = await parseJestJunit([xmlFixture], opts) const parser = new JestJunitParser(opts)
fs.mkdirSync(path.dirname(outputPath), {recursive: true}) const result = await parser.parse(filePath, fileContent)
fs.writeFileSync(outputPath, result?.output?.summary ?? '') expect(result).toMatchSnapshot()
expect(result.success).toBeFalsy() const report = getReport([result])
expect(result?.output).toMatchSnapshot() fs.mkdirSync(path.dirname(outputPath), {recursive: true})
fs.writeFileSync(outputPath, report)
})
it('report from facebook/jest test results matches snapshot', async () => {
const fixturePath = path.join(__dirname, 'fixtures', 'external', 'jest', 'jest-test-results.xml')
const trackedFilesPath = path.join(__dirname, 'fixtures', 'external', 'jest', 'files.txt')
const outputPath = path.join(__dirname, '__outputs__', 'jest-test-results.md')
const filePath = normalizeFilePath(path.relative(__dirname, fixturePath))
const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'})
const trackedFiles = fs.readFileSync(trackedFilesPath, {encoding: 'utf8'}).split(/\n\r?/g)
const opts: ParseOptions = {
parseErrors: true,
trackedFiles,
workDir: '/home/dorny/dorny/jest/'
}
const parser = new JestJunitParser(opts)
const result = await parser.parse(filePath, fileContent)
expect(result).toMatchSnapshot()
const report = getReport([result], {listTests: 'failed'})
fs.mkdirSync(path.dirname(outputPath), {recursive: true})
fs.writeFileSync(outputPath, report)
}) })
}) })

View file

@ -4,19 +4,14 @@ description: |
Supports .NET (xUnit, NUnit, MSTest), Dart, Flutter and JavaScript (JEST). Supports .NET (xUnit, NUnit, MSTest), Dart, Flutter and JavaScript (JEST).
author: Michal Dorner <dorner.michal@gmail.com> author: Michal Dorner <dorner.michal@gmail.com>
inputs: inputs:
annotations:
description: Enables code annotations with error message and stack trace captured during test execution
required: true
default: 'true'
fail-on-error:
description: Set this action as failed if test report contain any failed test
required: true
default: 'true'
name: name:
description: Name of the check run description: Name of the check run
required: true required: true
path: path:
description: Path to test report description: |
Coma separated list of paths to test reports
Supports wildcards via [fast-glob](https://github.com/mrmlnc/fast-glob)
All matched result files must be of same format
required: true required: true
reporter: reporter:
description: | description: |
@ -26,13 +21,38 @@ inputs:
- flutter-machine - flutter-machine
- jest-junit - jest-junit
required: true required: true
list-suites:
description: |
Limits which test suites are listed. Supported options:
- all
- only-failed
required: true
default: 'all'
list-tests:
description: |
Limits which test cases are listed. Supported options:
- all
- only-failed
- none
required: true
default: 'all'
max-annotations:
description: |
Limits number of created annotations with error message and stack trace captured during test execution.
Must be less or equal to 50.
required: true
default: '10'
fail-on-error:
description: Set this action as failed if test report contain any failed test
required: true
default: 'true'
working-directory:
description: Relative path under $GITHUB_WORKSPACE where the repository was checked out
required: false
token: token:
description: GitHub Access Token description: GitHub Access Token
required: false required: false
default: ${{ github.token }} default: ${{ github.token }}
working-directory:
description: Relative path under $GITHUB_WORKSPACE where the repository was checked out
required: false
outputs: outputs:
conclusion: conclusion:
description: | description: |

805
dist/index.js generated vendored

File diff suppressed because it is too large Load diff

2
dist/index.js.map generated vendored

File diff suppressed because one or more lines are too long

View file

@ -2,13 +2,20 @@ import * as core from '@actions/core'
import * as github from '@actions/github' import * as github from '@actions/github'
import * as fs from 'fs' import * as fs from 'fs'
import glob from 'fast-glob' import glob from 'fast-glob'
import {parseDartJson} from './parsers/dart-json/dart-json-parser'
import {parseDotnetTrx} from './parsers/dotnet-trx/dotnet-trx-parser' import {ParseOptions, TestParser} from './test-parser'
import {parseJestJunit} from './parsers/jest-junit/jest-junit-parser' import {TestRunResult} from './test-results'
import {FileContent, ParseOptions, ParseTestResult} from './parsers/parser-types' import {getAnnotations} from './report/get-annotations'
import {getReport} from './report/get-report'
import {DartJsonParser} from './parsers/dart-json/dart-json-parser'
import {DotnetTrxParser} from './parsers/dotnet-trx/dotnet-trx-parser'
import {JestJunitParser} from './parsers/jest-junit/jest-junit-parser'
import {normalizeDirPath} from './utils/file-utils' import {normalizeDirPath} from './utils/file-utils'
import {listFiles} from './utils/git' import {listFiles} from './utils/git'
import {getCheckRunSha} from './utils/github-utils' import {getCheckRunSha} from './utils/github-utils'
import {Icon} from './utils/markdown-utils'
async function run(): Promise<void> { async function run(): Promise<void> {
try { try {
@ -19,13 +26,30 @@ async function run(): Promise<void> {
} }
async function main(): Promise<void> { async function main(): Promise<void> {
const annotations = core.getInput('annotations', {required: true}) === 'true'
const failOnError = core.getInput('fail-on-error', {required: true}) === 'true'
const name = core.getInput('name', {required: true}) const name = core.getInput('name', {required: true})
const path = core.getInput('path', {required: true}) const path = core.getInput('path', {required: true})
const reporter = core.getInput('reporter', {required: true}) const reporter = core.getInput('reporter', {required: true})
const token = core.getInput('token', {required: true}) const listSuites = core.getInput('list-suites', {required: true})
const listTests = core.getInput('list-tests', {required: true})
const maxAnnotations = parseInt(core.getInput('max-annotations', {required: true}))
const failOnError = core.getInput('fail-on-error', {required: true}) === 'true'
const workDirInput = core.getInput('working-directory', {required: false}) const workDirInput = core.getInput('working-directory', {required: false})
const token = core.getInput('token', {required: true})
if (listSuites !== 'all' && listSuites !== 'failed') {
core.setFailed(`Input parameter 'list-suites' has invalid value`)
return
}
if (listTests !== 'all' && listTests !== 'failed' && listTests !== 'none') {
core.setFailed(`Input parameter 'list-tests' has invalid value`)
return
}
if (isNaN(maxAnnotations) || maxAnnotations < 0 || maxAnnotations > 50) {
core.setFailed(`Input parameter 'max-annotations' has invalid value`)
return
}
if (workDirInput) { if (workDirInput) {
core.info(`Changing directory to ${workDirInput}`) core.info(`Changing directory to ${workDirInput}`)
@ -37,26 +61,41 @@ async function main(): Promise<void> {
const sha = getCheckRunSha() const sha = getCheckRunSha()
// We won't need tracked files if we are not going to create annotations // We won't need tracked files if we are not going to create annotations
const trackedFiles = annotations ? await listFiles() : [] const parseErrors = maxAnnotations > 0
const trackedFiles = parseErrors ? await listFiles() : []
const opts: ParseOptions = { const options: ParseOptions = {
name,
annotations,
trackedFiles, trackedFiles,
workDir workDir,
parseErrors
} }
const parser = getParser(reporter) core.info(`Using test report parser '${reporter}'`)
const files = await getFiles(path) const parser = getParser(reporter, options)
const files = await getFiles(path)
if (files.length === 0) { if (files.length === 0) {
core.setFailed(`No file matches path '${path}'`) core.setFailed(`No file matches path '${path}'`)
return return
} }
core.info(`Using test report parser '${reporter}'`) const results: TestRunResult[] = []
const result = await parser(files, opts) for (const file of files) {
const conclusion = result.success ? 'success' : 'failure' core.info(`Processing test report ${file}`)
const content = await fs.promises.readFile(file, {encoding: 'utf8'})
const tr = await parser.parse(file, content)
results.push(tr)
}
core.info('Creating report summary')
const summary = getReport(results, {listSuites, listTests})
core.info('Creating annotations')
const annotations = getAnnotations(results, maxAnnotations)
const isFailed = results.some(tr => tr.result === 'failed')
const conclusion = isFailed ? 'failure' : 'success'
const icon = isFailed ? Icon.fail : Icon.success
core.info(`Creating check run '${name}' with conclusion '${conclusion}'`) core.info(`Creating check run '${name}' with conclusion '${conclusion}'`)
await octokit.checks.create({ await octokit.checks.create({
@ -64,40 +103,49 @@ async function main(): Promise<void> {
name, name,
conclusion, conclusion,
status: 'completed', status: 'completed',
output: result.output, output: {
title: `${name} ${icon}`,
summary,
annotations
},
...github.context.repo ...github.context.repo
}) })
const passed = results.reduce((sum, tr) => sum + tr.passed, 0)
const failed = results.reduce((sum, tr) => sum + tr.failed, 0)
const skipped = results.reduce((sum, tr) => sum + tr.skipped, 0)
const time = results.reduce((sum, tr) => sum + tr.time, 0)
core.setOutput('conclusion', conclusion) core.setOutput('conclusion', conclusion)
if (failOnError && !result.success) { core.setOutput('passed', passed)
core.setOutput('failed', failed)
core.setOutput('skipped', skipped)
core.setOutput('time', time)
if (failOnError && isFailed) {
core.setFailed(`Failed test has been found and 'fail-on-error' option is set to ${failOnError}`) core.setFailed(`Failed test has been found and 'fail-on-error' option is set to ${failOnError}`)
} }
} }
function getParser(reporter: string): ParseTestResult { function getParser(reporter: string, options: ParseOptions): TestParser {
switch (reporter) { switch (reporter) {
case 'dart-json': case 'dart-json':
return parseDartJson return new DartJsonParser(options)
case 'dotnet-trx': case 'dotnet-trx':
return parseDotnetTrx return new DotnetTrxParser(options)
case 'flutter-machine': case 'flutter-machine':
return parseDartJson return new DartJsonParser(options)
case 'jest-junit': case 'jest-junit':
return parseJestJunit return new JestJunitParser(options)
default: default:
throw new Error(`Input variable 'reporter' is set to invalid value '${reporter}'`) throw new Error(`Input variable 'reporter' is set to invalid value '${reporter}'`)
} }
} }
export async function getFiles(pattern: string): Promise<FileContent[]> { export async function getFiles(pattern: string): Promise<string[]> {
const paths = await glob(pattern, {dot: true}) const tasks = pattern.split(',').map(async pat => glob(pat, {dot: true}))
return Promise.all( const paths = await Promise.all(tasks)
paths.map(async path => { return paths.flat()
core.info(`Reading test report '${path}'`)
const content = await fs.promises.readFile(path, {encoding: 'utf8'})
return {path, content}
})
)
} }
run() run()

View file

@ -1,9 +1,7 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import {Annotation, FileContent, ParseOptions, TestResult} from '../parser-types' import {ParseOptions, TestParser} from '../../test-parser'
import getReport from '../../report/get-report'
import {normalizeFilePath} from '../../utils/file-utils' import {normalizeFilePath} from '../../utils/file-utils'
import {Icon, fixEol} from '../../utils/markdown-utils'
import { import {
ReportEvent, ReportEvent,
@ -25,8 +23,9 @@ import {
TestRunResult, TestRunResult,
TestSuiteResult, TestSuiteResult,
TestGroupResult, TestGroupResult,
TestCaseResult TestCaseResult,
} from '../../report/test-results' TestCaseError
} from '../../test-results'
class TestRun { class TestRun {
constructor(readonly path: string, readonly suites: TestSuite[], readonly success: boolean, readonly time: number) {} constructor(readonly path: string, readonly suites: TestSuite[], readonly success: boolean, readonly time: number) {}
@ -69,23 +68,16 @@ class TestCase {
} }
} }
export async function parseDartJson(files: FileContent[], options: ParseOptions): Promise<TestResult> { export class DartJsonParser implements TestParser {
const testRuns = files.map(f => getTestRun(f.path, f.content)) constructor(readonly options: ParseOptions) {}
const testRunsResults = testRuns.map(getTestRunResult)
const success = testRuns.every(tr => tr.success)
const icon = success ? Icon.success : Icon.fail
return { async parse(path: string, content: string): Promise<TestRunResult> {
success, const tr = this.getTestRun(path, content)
output: { const result = this.getTestRunResult(tr)
title: `${options.name.trim()} ${icon}`, return Promise.resolve(result)
summary: getReport(testRunsResults),
annotations: options.annotations ? getAnnotations(testRuns, options.workDir, options.trackedFiles) : undefined
}
}
} }
function getTestRun(path: string, content: string): TestRun { private getTestRun(path: string, content: string): TestRun {
core.info(`Parsing content of '${path}'`) core.info(`Parsing content of '${path}'`)
const lines = content.split(/\n\r?/g) const lines = content.split(/\n\r?/g)
const events = lines const events = lines
@ -131,75 +123,61 @@ function getTestRun(path: string, content: string): TestRun {
return new TestRun(path, Object.values(suites), success, totalTime) return new TestRun(path, Object.values(suites), success, totalTime)
} }
function getTestRunResult(tr: TestRun): TestRunResult { private getTestRunResult(tr: TestRun): TestRunResult {
const suites = tr.suites.map(s => { const suites = tr.suites.map(s => {
return new TestSuiteResult(s.suite.path, getGroups(s)) return new TestSuiteResult(s.suite.path, this.getGroups(s))
}) })
return new TestRunResult(tr.path, suites, tr.time) return new TestRunResult(tr.path, suites, tr.time)
} }
function getGroups(suite: TestSuite): TestGroupResult[] { private getGroups(suite: TestSuite): TestGroupResult[] {
const groups = Object.values(suite.groups).filter(grp => grp.tests.length > 0) const groups = Object.values(suite.groups).filter(grp => grp.tests.length > 0)
groups.sort((a, b) => (a.group.line ?? 0) - (b.group.line ?? 0)) groups.sort((a, b) => (a.group.line ?? 0) - (b.group.line ?? 0))
return groups.map(group => { return groups.map(group => {
group.tests.sort((a, b) => (a.testStart.test.line ?? 0) - (b.testStart.test.line ?? 0)) group.tests.sort((a, b) => (a.testStart.test.line ?? 0) - (b.testStart.test.line ?? 0))
const tests = group.tests.map(t => new TestCaseResult(t.testStart.test.name, t.result, t.time)) const tests = group.tests.map(t => this.getTest(t))
return new TestGroupResult(group.group.name, tests) return new TestGroupResult(group.group.name, tests)
}) })
} }
function getAnnotations(testRuns: TestRun[], workDir: string, trackedFiles: string[]): Annotation[] { private getTest(tc: TestCase): TestCaseResult {
const annotations: Annotation[] = [] const error = this.getError(tc)
for (const tr of testRuns) { return new TestCaseResult(tc.testStart.test.name, tc.result, tc.time, error)
for (const suite of tr.suites) {
for (const group of Object.values(suite.groups)) {
for (const test of group.tests) {
if (test.error) {
const err = getAnnotation(test, suite, workDir, trackedFiles)
if (err !== null) {
annotations.push(err)
}
}
}
}
}
} }
return annotations private getError(test: TestCase): TestCaseError | undefined {
if (!this.options.parseErrors || !test.error) {
return undefined
} }
function getAnnotation( const {workDir, trackedFiles} = this.options
test: TestCase, const message = test.error?.error ?? ''
testSuite: TestSuite, const stackTrace = test.error?.stackTrace ?? ''
workDir: string, const src = this.exceptionThrowSource(stackTrace, trackedFiles)
trackedFiles: string[] let path
): Annotation | null { let line
const stack = test.error?.stackTrace ?? ''
let src = exceptionThrowSource(stack, trackedFiles) if (src !== undefined) {
if (src === null) { ;(path = src.path), (line = src.line)
const file = getRelativePathFromUrl(test.testStart.test.url ?? '', workDir) } else {
if (!trackedFiles.includes(file)) { const testStartPath = this.getRelativePathFromUrl(test.testStart.test.url ?? '', workDir)
return null if (trackedFiles.includes(testStartPath)) {
} path = testStartPath
src = {
file,
line: test.testStart.test.line ?? 0
} }
line = test.testStart.test.line ?? undefined
} }
return { return {
annotation_level: 'failure', path,
start_line: src.line, line,
end_line: src.line, message,
path: src.file, stackTrace
message: `${fixEol(test.error?.error)}\n\n${fixEol(test.error?.stackTrace)}`,
title: `[${testSuite.suite.path}] ${test.testStart.test.name}`
} }
} }
function exceptionThrowSource(ex: string, trackedFiles: string[]): {file: string; line: number} | null { private exceptionThrowSource(ex: string, trackedFiles: string[]): {path: string; line: number} | undefined {
// imports from package which is tested are listed in stack traces as 'package:xyz/' which maps to relative path 'lib/' // imports from package which is tested are listed in stack traces as 'package:xyz/' which maps to relative path 'lib/'
const packageRe = /^package:[a-zA-z0-9_$]+\// const packageRe = /^package:[a-zA-z0-9_$]+\//
const lines = ex.split(/\r?\n/).map(str => str.replace(packageRe, 'lib/')) const lines = ex.split(/\r?\n/).map(str => str.replace(packageRe, 'lib/'))
@ -209,25 +187,24 @@ function exceptionThrowSource(ex: string, trackedFiles: string[]): {file: string
for (const str of lines) { for (const str of lines) {
const match = str.match(re) const match = str.match(re)
if (match !== null) { if (match !== null) {
const [_, fileStr, lineStr] = match const [_, pathStr, lineStr] = match
const file = normalizeFilePath(fileStr) const path = normalizeFilePath(pathStr)
if (trackedFiles.includes(file)) { if (trackedFiles.includes(path)) {
const line = parseInt(lineStr) const line = parseInt(lineStr)
return {file, line} return {path, line}
}
} }
} }
} }
return null private getRelativePathFromUrl(file: string, workDir: string): string {
}
function getRelativePathFromUrl(file: string, workdir: string): string {
const prefix = 'file:///' const prefix = 'file:///'
if (file.startsWith(prefix)) { if (file.startsWith(prefix)) {
file = file.substr(prefix.length) file = file.substr(prefix.length)
} }
if (file.startsWith(workdir)) { if (file.startsWith(workDir)) {
file = file.substr(workdir.length) file = file.substr(workDir.length)
} }
return file return file
} }
}

View file

@ -1,21 +1,20 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import {ErrorInfo, Outcome, TestMethod, TrxReport} from './dotnet-trx-types'
import {Annotation, FileContent, ParseOptions, TestResult} from '../parser-types'
import {parseStringPromise} from 'xml2js' import {parseStringPromise} from 'xml2js'
import {ErrorInfo, Outcome, TestMethod, TrxReport} from './dotnet-trx-types'
import {ParseOptions, TestParser} from '../../test-parser'
import {normalizeFilePath} from '../../utils/file-utils' import {normalizeFilePath} from '../../utils/file-utils'
import {parseAttribute} from '../../utils/xml-utils' import {parseIsoDate, parseNetDuration} from '../../utils/parse-utils'
import {Icon, fixEol} from '../../utils/markdown-utils'
import { import {
TestExecutionResult, TestExecutionResult,
TestRunResult, TestRunResult,
TestSuiteResult, TestSuiteResult,
TestGroupResult, TestGroupResult,
TestCaseResult TestCaseResult,
} from '../../report/test-results' TestCaseError
import getReport from '../../report/get-report' } from '../../test-results'
class TestClass { class TestClass {
constructor(readonly name: string) {} constructor(readonly name: string) {}
@ -42,56 +41,26 @@ class Test {
} }
} }
export async function parseDotnetTrx(files: FileContent[], options: ParseOptions): Promise<TestResult> { export class DotnetTrxParser implements TestParser {
const testRuns: TestRunResult[] = [] constructor(readonly options: ParseOptions) {}
const testClasses: TestClass[] = []
for (const file of files) { async parse(path: string, content: string): Promise<TestRunResult> {
const trx = await getTrxReport(file) const trx = await this.getTrxReport(path, content)
const tc = getTestClasses(trx) const tc = this.getTestClasses(trx)
const tr = getTestRunResult(file.path, trx, tc) const tr = this.getTestRunResult(path, trx, tc)
testRuns.push(tr) return tr
testClasses.push(...tc)
} }
const success = testRuns.every(tr => tr.result === 'success') private async getTrxReport(path: string, content: string): Promise<TrxReport> {
const icon = success ? Icon.success : Icon.fail core.info(`Parsing content of '${path}'`)
return {
success,
output: {
title: `${options.name.trim()} ${icon}`,
summary: getReport(testRuns),
annotations: options.annotations ? getAnnotations(testClasses, options.workDir, options.trackedFiles) : undefined
}
}
}
async function getTrxReport(file: FileContent): Promise<TrxReport> {
core.info(`Parsing content of '${file.path}'`)
try { try {
return (await parseStringPromise(file.content, { return (await parseStringPromise(content)) as TrxReport
attrValueProcessors: [parseAttribute]
})) as TrxReport
} catch (e) { } catch (e) {
throw new Error(`Invalid XML at ${file.path}\n\n${e}`) throw new Error(`Invalid XML at ${path}\n\n${e}`)
} }
} }
function getTestRunResult(path: string, trx: TrxReport, testClasses: TestClass[]): TestRunResult { private getTestClasses(trx: TrxReport): TestClass[] {
const times = trx.TestRun.Times[0].$
const totalTime = times.finish.getTime() - times.start.getTime()
const suites = testClasses.map(tc => {
const tests = tc.tests.map(t => new TestCaseResult(t.name, t.result, t.duration))
const group = new TestGroupResult(null, tests)
return new TestSuiteResult(tc.name, [group])
})
return new TestRunResult(path, suites, totalTime)
}
function getTestClasses(trx: TrxReport): TestClass[] {
const unitTests: {[id: string]: TestMethod} = {} const unitTests: {[id: string]: TestMethod} = {}
for (const td of trx.TestRun.TestDefinitions) { for (const td of trx.TestRun.TestDefinitions) {
for (const ut of td.UnitTest) { for (const ut of td.UnitTest) {
@ -113,7 +82,8 @@ function getTestClasses(trx: TrxReport): TestClass[] {
} }
const output = r.unitTestResult.Output const output = r.unitTestResult.Output
const error = output?.length > 0 && output[0].ErrorInfo?.length > 0 ? output[0].ErrorInfo[0] : undefined const error = output?.length > 0 && output[0].ErrorInfo?.length > 0 ? output[0].ErrorInfo[0] : undefined
const test = new Test(r.testMethod.$.name, r.unitTestResult.$.outcome, r.unitTestResult.$.duration, error) const duration = parseNetDuration(r.unitTestResult.$.duration)
const test = new Test(r.testMethod.$.name, r.unitTestResult.$.outcome, duration, error)
tc.tests.push(test) tc.tests.push(test)
} }
@ -126,36 +96,50 @@ function getTestClasses(trx: TrxReport): TestClass[] {
return result return result
} }
function getAnnotations(testClasses: TestClass[], workDir: string, trackedFiles: string[]): Annotation[] { private getTestRunResult(path: string, trx: TrxReport, testClasses: TestClass[]): TestRunResult {
const annotations: Annotation[] = [] const times = trx.TestRun.Times[0].$
for (const tc of testClasses) { const totalTime = parseIsoDate(times.finish).getTime() - parseIsoDate(times.start).getTime()
for (const t of tc.tests) {
if (t.error) { const suites = testClasses.map(testClass => {
const src = exceptionThrowSource(t.error.StackTrace[0], workDir, trackedFiles) const tests = testClass.tests.map(test => {
if (src === null) { const error = this.getError(test)
continue return new TestCaseResult(test.name, test.result, test.duration, error)
}
annotations.push({
annotation_level: 'failure',
start_line: src.line,
end_line: src.line,
path: src.file,
message: fixEol(t.error.Message[0]),
title: `[${tc.name}] ${t.name}`
}) })
} const group = new TestGroupResult(null, tests)
} return new TestSuiteResult(testClass.name, [group])
} })
return annotations
return new TestRunResult(path, suites, totalTime)
} }
export function exceptionThrowSource( private getError(test: Test): TestCaseError | undefined {
ex: string, if (!this.options.parseErrors || !test.error) {
workDir: string, return undefined
trackedFiles: string[] }
): {file: string; line: number} | null {
const lines = ex.split(/\r*\n/) const message = test.error.Message[0]
const stackTrace = test.error.StackTrace[0]
let path
let line
const src = this.exceptionThrowSource(stackTrace)
if (src) {
path = src.path
line = src.line
}
return {
path,
line,
message,
stackTrace: `${message}\n${stackTrace}`
}
}
private exceptionThrowSource(stackTrace: string): {path: string; line: number} | undefined {
const lines = stackTrace.split(/\r*\n/)
const re = / in (.+):line (\d+)$/ const re = / in (.+):line (\d+)$/
const {workDir, trackedFiles} = this.options
for (const str of lines) { for (const str of lines) {
const match = str.match(re) const match = str.match(re)
@ -165,10 +149,9 @@ export function exceptionThrowSource(
const file = filePath.startsWith(workDir) ? filePath.substr(workDir.length) : filePath const file = filePath.startsWith(workDir) ? filePath.substr(workDir.length) : filePath
if (trackedFiles.includes(file)) { if (trackedFiles.includes(file)) {
const line = parseInt(lineStr) const line = parseInt(lineStr)
return {file, line} return {path: file, line}
}
} }
} }
} }
return null
} }

View file

@ -10,10 +10,10 @@ export interface TestRun {
export interface Times { export interface Times {
$: { $: {
creation: Date creation: string
queuing: Date queuing: string
start: Date start: string
finish: Date finish: string
} }
} }
@ -43,7 +43,7 @@ export interface UnitTestResult {
$: { $: {
testId: string testId: string
testName: string testName: string
duration: number duration: string
outcome: Outcome outcome: Outcome
} }
Output: Output[] Output: Output[]

View file

@ -1,69 +1,49 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import {Annotation, FileContent, ParseOptions, TestResult} from '../parser-types' import {ParseOptions, TestParser} from '../../test-parser'
import {parseStringPromise} from 'xml2js' import {parseStringPromise} from 'xml2js'
import {JunitReport, TestCase, TestSuite} from './jest-junit-types' import {JunitReport, TestCase, TestSuite} from './jest-junit-types'
import {fixEol, Icon} from '../../utils/markdown-utils'
import {normalizeFilePath} from '../../utils/file-utils' import {normalizeFilePath} from '../../utils/file-utils'
import {parseAttribute} from '../../utils/xml-utils'
import { import {
TestExecutionResult, TestExecutionResult,
TestRunResult, TestRunResult,
TestSuiteResult, TestSuiteResult,
TestGroupResult, TestGroupResult,
TestCaseResult TestCaseResult,
} from '../../report/test-results' TestCaseError
import getReport from '../../report/get-report' } from '../../test-results'
export async function parseJestJunit(files: FileContent[], options: ParseOptions): Promise<TestResult> { export class JestJunitParser implements TestParser {
const junit: JunitReport[] = [] constructor(readonly options: ParseOptions) {}
const testRuns: TestRunResult[] = []
for (const file of files) { async parse(path: string, content: string): Promise<TestRunResult> {
const ju = await getJunitReport(file) const ju = await this.getJunitReport(path, content)
const tr = getTestRunResult(file.path, ju) return this.getTestRunResult(path, ju)
junit.push(ju)
testRuns.push(tr)
} }
const success = testRuns.every(tr => tr.result === 'success') private async getJunitReport(path: string, content: string): Promise<JunitReport> {
const icon = success ? Icon.success : Icon.fail core.info(`Parsing content of '${path}'`)
return {
success,
output: {
title: `${options.name.trim()} ${icon}`,
summary: getReport(testRuns),
annotations: options.annotations ? getAnnotations(junit, options.workDir, options.trackedFiles) : undefined
}
}
}
async function getJunitReport(file: FileContent): Promise<JunitReport> {
core.info(`Parsing content of '${file.path}'`)
try { try {
return (await parseStringPromise(file.content, { return (await parseStringPromise(content)) as JunitReport
attrValueProcessors: [parseAttribute]
})) as JunitReport
} catch (e) { } catch (e) {
throw new Error(`Invalid XML at ${file.path}\n\n${e}`) throw new Error(`Invalid XML at ${path}\n\n${e}`)
} }
} }
function getTestRunResult(path: string, junit: JunitReport): TestRunResult { private getTestRunResult(path: string, junit: JunitReport): TestRunResult {
const suites = junit.testsuites.testsuite.map(ts => { const suites = junit.testsuites.testsuite.map(ts => {
const name = ts.$.name.trim() const name = ts.$.name.trim()
const time = ts.$.time * 1000 const time = parseFloat(ts.$.time) * 1000
const sr = new TestSuiteResult(name, getGroups(ts), time) const sr = new TestSuiteResult(name, this.getGroups(ts), time)
return sr return sr
}) })
const time = junit.testsuites.$.time * 1000 const time = parseFloat(junit.testsuites.$.time) * 1000
return new TestRunResult(path, suites, time) return new TestRunResult(path, suites, time)
} }
function getGroups(suite: TestSuite): TestGroupResult[] { private getGroups(suite: TestSuite): TestGroupResult[] {
const groups: {describe: string; tests: TestCase[]}[] = [] const groups: {describe: string; tests: TestCase[]}[] = []
for (const tc of suite.testcase) { for (const tc of suite.testcase) {
let grp = groups.find(g => g.describe === tc.$.classname) let grp = groups.find(g => g.describe === tc.$.classname)
@ -77,69 +57,60 @@ function getGroups(suite: TestSuite): TestGroupResult[] {
return groups.map(grp => { return groups.map(grp => {
const tests = grp.tests.map(tc => { const tests = grp.tests.map(tc => {
const name = tc.$.name.trim() const name = tc.$.name.trim()
const result = getTestCaseResult(tc) const result = this.getTestCaseResult(tc)
const time = tc.$.time * 1000 const time = parseFloat(tc.$.time) * 1000
return new TestCaseResult(name, result, time) const error = this.getTestCaseError(tc)
return new TestCaseResult(name, result, time, error)
}) })
return new TestGroupResult(grp.describe, tests) return new TestGroupResult(grp.describe, tests)
}) })
} }
function getTestCaseResult(test: TestCase): TestExecutionResult { private getTestCaseResult(test: TestCase): TestExecutionResult {
if (test.failure) return 'failed' if (test.failure) return 'failed'
if (test.skipped) return 'skipped' if (test.skipped) return 'skipped'
return 'success' return 'success'
} }
function getAnnotations(junitReports: JunitReport[], workDir: string, trackedFiles: string[]): Annotation[] { private getTestCaseError(tc: TestCase): TestCaseError | undefined {
const annotations: Annotation[] = [] if (!this.options.parseErrors || !tc.failure) {
for (const junit of junitReports) { return undefined
for (const suite of junit.testsuites.testsuite) {
for (const tc of suite.testcase) {
if (!tc.failure) {
continue
}
for (const ex of tc.failure) {
const src = exceptionThrowSource(ex, workDir, trackedFiles)
if (src === null) {
continue
}
annotations.push({
annotation_level: 'failure',
start_line: src.line,
end_line: src.line,
path: src.file,
message: fixEol(ex),
title: `[${suite.$.name}] ${tc.$.name.trim()}`
})
}
}
}
}
return annotations
} }
export function exceptionThrowSource( const stackTrace = tc.failure[0]
ex: string, let path
workDir: string, let line
trackedFiles: string[]
): {file: string; line: number; column: number} | null {
const lines = ex.split(/\r?\n/)
const re = /\((.*):(\d+):(\d+)\)$/
const src = this.exceptionThrowSource(stackTrace)
if (src) {
path = src.path
line = src.line
}
return {
path,
line,
stackTrace
}
}
private exceptionThrowSource(stackTrace: string): {path: string; line: number} | undefined {
const lines = stackTrace.split(/\r?\n/)
const re = /\((.*):(\d+):\d+\)$/
const {workDir, trackedFiles} = this.options
for (const str of lines) { for (const str of lines) {
const match = str.match(re) const match = str.match(re)
if (match !== null) { if (match !== null) {
const [_, fileStr, lineStr, colStr] = match const [_, fileStr, lineStr] = match
const filePath = normalizeFilePath(fileStr) const filePath = normalizeFilePath(fileStr)
const file = filePath.startsWith(workDir) ? filePath.substr(workDir.length) : filePath const path = filePath.startsWith(workDir) ? filePath.substr(workDir.length) : filePath
if (trackedFiles.includes(file)) { if (trackedFiles.includes(path)) {
const line = parseInt(lineStr) const line = parseInt(lineStr)
const column = parseInt(colStr)
return {file, line, column}
}
}
}
return null return {path, line}
}
}
}
}
} }

View file

@ -5,10 +5,10 @@ export interface JunitReport {
export interface TestSuites { export interface TestSuites {
$: { $: {
name: string name: string
tests: number tests: string
failures: number // assertion failed failures: string // assertion failed
errors: number // unhandled exception during test execution errors: string // unhandled exception during test execution
time: number time: string
} }
testsuite: TestSuite[] testsuite: TestSuite[]
} }
@ -16,11 +16,11 @@ export interface TestSuites {
export interface TestSuite { export interface TestSuite {
$: { $: {
name: string name: string
tests: number tests: string
errors: number errors: string
failures: number failures: string
skipped: number skipped: string
time: number time: string
timestamp?: Date timestamp?: Date
} }
testcase: TestCase[] testcase: TestCase[]
@ -31,7 +31,7 @@ export interface TestCase {
classname: string classname: string
file?: string file?: string
name: string name: string
time: number time: string
} }
failure?: string[] failure?: string[]
skipped?: string[] skipped?: string[]

View file

@ -1,30 +0,0 @@
import {Endpoints} from '@octokit/types'
export type OutputParameters = Endpoints['POST /repos/{owner}/{repo}/check-runs']['parameters']['output']
export type Annotation = {
path: string
start_line: number
end_line: number
start_column?: number
end_column?: number
annotation_level: 'notice' | 'warning' | 'failure'
message: string
title?: string
raw_details?: string
}
export type ParseTestResult = (files: FileContent[], options: ParseOptions) => Promise<TestResult>
export type FileContent = {path: string; content: string}
export interface ParseOptions {
name: string
annotations: boolean
workDir: string
trackedFiles: string[]
}
export interface TestResult {
success: boolean
output: OutputParameters
}

View file

@ -0,0 +1,111 @@
import {ellipsis, fixEol} from '../utils/markdown-utils'
import {TestRunResult} from '../test-results'
type Annotation = {
path: string
start_line: number
end_line: number
start_column?: number
end_column?: number
annotation_level: 'notice' | 'warning' | 'failure'
message: string
title?: string
raw_details?: string
}
interface TestError {
testRunPaths: string[]
suiteName: string
testName: string
path: string
line: number
stackTrace: string
message: string
}
export function getAnnotations(results: TestRunResult[], maxCount: number): Annotation[] {
if (maxCount === 0) {
return []
}
// Collect errors from TestRunResults
// Merge duplicates if there are more test results files processed
const errors: TestError[] = []
const mergeDup = results.length > 1
for (const tr of results) {
for (const ts of tr.suites) {
for (const tg of ts.groups) {
for (const tc of tg.tests) {
const err = tc.error
if (err === undefined) {
continue
}
const path = err.path ?? tr.path
const line = err.line ?? 0
if (mergeDup) {
const dup = errors.find(e => path === e.path && line === e.line && err.stackTrace === e.stackTrace)
if (dup !== undefined) {
dup.testRunPaths.push(tr.path)
continue
}
}
errors.push({
testRunPaths: [tr.path],
suiteName: ts.name,
testName: tc.name,
stackTrace: err.stackTrace,
message: err.message ?? getFirstNonEmptyLine(err.stackTrace) ?? 'Test failed',
path,
line
})
}
}
}
}
// Limit number of created annotations
errors.splice(maxCount + 1)
const annotations = errors.map(e => {
const message = [
'Failed test found in:',
e.testRunPaths.map(p => ` ${p}`).join('\n'),
'Error:',
ident(fixEol(e.message), ' ')
].join('\n')
return enforceCheckRunLimits({
path: e.path,
start_line: e.line,
end_line: e.line,
annotation_level: 'failure',
title: `${e.suiteName}${e.testName}`,
raw_details: fixEol(e.stackTrace),
message
})
})
return annotations
}
function enforceCheckRunLimits(err: Annotation): Annotation {
err.title = ellipsis(err.title || '', 255)
err.message = ellipsis(err.message, 65535)
if (err.raw_details) {
err.raw_details = ellipsis(err.raw_details, 65535)
}
return err
}
function getFirstNonEmptyLine(stackTrace: string): string | undefined {
const lines = stackTrace.split(/\r?\n/g)
return lines.find(str => !/^\s*$/.test(str))
}
function ident(text: string, prefix: string): string {
return text
.split(/\n/g)
.map(line => prefix + line)
.join('\n')
}

View file

@ -1,94 +1,201 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import {TestExecutionResult, TestRunResult, TestSuiteResult} from './test-results' import {TestExecutionResult, TestRunResult, TestSuiteResult} from '../test-results'
import {Align, Icon, link, table} from '../utils/markdown-utils' import {Align, formatTime, Icon, link, table} from '../utils/markdown-utils'
import {slug} from '../utils/slugger' import {slug} from '../utils/slugger'
export default function getReport(results: TestRunResult[]): string { export interface ReportOptions {
const badge = getBadge(results) listSuites?: 'all' | 'failed'
const runsSummary = results.map(getRunSummary).join('\n\n') listTests?: 'all' | 'failed' | 'none'
const suites = results
.flatMap(tr => tr.suites)
.map((ts, i) => getSuiteSummary(ts, i))
.join('\n')
const suitesSection = `# Test Suites\n\n${suites}`
return [badge, runsSummary, suitesSection].join('\n\n')
} }
function getBadge(results: TestRunResult[]): string { export function getReport(results: TestRunResult[], options: ReportOptions = {}): string {
core.info('Generating check run summary')
const maxReportLength = 65535
const sections: string[] = []
applySort(results)
const badge = getReportBadge(results)
sections.push(badge)
const runs = getTestRunsReport(results, options)
sections.push(...runs)
const report = sections.join('\n')
if (report.length > maxReportLength) {
let msg = `**Check Run summary limit of ${maxReportLength} chars was exceed**`
if (options.listTests !== 'all') {
msg += '\n- Consider setting `list-tests` option to `only-failed` or `none`'
}
if (options.listSuites !== 'all') {
msg += '\n- Consider setting `list-suites` option to `only-failed`'
}
return `${badge}\n${msg}`
}
return report
}
function applySort(results: TestRunResult[]): void {
results.sort((a, b) => a.path.localeCompare(b.path))
for (const res of results) {
res.suites.sort((a, b) => a.name.localeCompare(b.name))
}
}
function getReportBadge(results: TestRunResult[]): string {
const passed = results.reduce((sum, tr) => sum + tr.passed, 0) const passed = results.reduce((sum, tr) => sum + tr.passed, 0)
const skipped = results.reduce((sum, tr) => sum + tr.skipped, 0) const skipped = results.reduce((sum, tr) => sum + tr.skipped, 0)
const failed = results.reduce((sum, tr) => sum + tr.failed, 0) const failed = results.reduce((sum, tr) => sum + tr.failed, 0)
return getBadge(passed, failed, skipped)
}
function getBadge(passed: number, failed: number, skipped: number): string {
const text = []
if (passed > 0) {
text.push(`${passed} passed`)
}
if (failed > 0) {
text.push(`${failed} failed`)
}
if (skipped > 0) {
text.push(`${skipped} skipped`)
}
const message = text.length > 0 ? text.join(', ') : 'none'
const passedText = passed > 0 ? `${passed} passed` : null
const skippedText = skipped > 0 ? `${skipped} skipped` : null
const failedText = failed > 0 ? `${failed} failed` : null
const message = [passedText, skippedText, failedText].filter(s => s != null).join(', ') || 'none'
let color = 'success' let color = 'success'
if (failed > 0) { if (failed > 0) {
color = 'critical' color = 'critical'
} else if (passed === 0 && failed === 0) { } else if (passed === 0 && failed === 0) {
color = 'yellow' color = 'yellow'
} }
const hint = failed > 0 ? 'Tests failed' : 'Tests passed successfully'
const uri = encodeURIComponent(`tests-${message}-${color}`) const uri = encodeURIComponent(`tests-${message}-${color}`)
const text = failed > 0 ? 'Tests failed' : 'Tests passed successfully' return `![${hint}](https://img.shields.io/badge/${uri})`
return `![${text}](https://img.shields.io/badge/${uri})`
} }
function getRunSummary(tr: TestRunResult): string { function getTestRunsReport(testRuns: TestRunResult[], options: ReportOptions): string[] {
core.info('Generating check run summary') const sections: string[] = []
const time = `${(tr.time / 1000).toFixed(3)}s`
const headingLine1 = `### ${tr.path}`
const headingLine2 = `**${tr.tests}** tests were completed in **${time}** with **${tr.passed}** passed, **${tr.skipped}** skipped and **${tr.failed}** failed.`
const suitesSummary = tr.suites.map((s, i) => { if (testRuns.length > 1) {
const icon = getResultIcon(s.result) const tableData = testRuns.map((tr, runIndex) => {
const tsTime = `${s.time}ms` const time = formatTime(tr.time)
const tsName = s.name const name = tr.path
const tsAddr = makeSuiteSlug(i, tsName).link const addr = makeRunSlug(runIndex).link
const tsNameLink = link(tsName, tsAddr) const nameLink = link(name, addr)
return [icon, tsNameLink, s.tests, tsTime, s.passed, s.skipped, s.failed] const passed = tr.passed > 0 ? `${tr.passed}${Icon.success}` : ''
const failed = tr.failed > 0 ? `${tr.failed}${Icon.fail}` : ''
const skipped = tr.skipped > 0 ? `${tr.skipped}${Icon.skip}` : ''
return [nameLink, passed, failed, skipped, time]
}) })
const summary = table( const resultsTable = table(
['Result', 'Suite', 'Tests', 'Time', `Passed ${Icon.success}`, `Skipped ${Icon.skip}`, `Failed ${Icon.fail}`], ['Report', 'Passed', 'Failed', 'Skipped', 'Time'],
[Align.Center, Align.Left, Align.Right, Align.Right, Align.Right, Align.Right, Align.Right], [Align.Left, Align.Right, Align.Right, Align.Right, Align.Right],
...suitesSummary ...tableData
) )
sections.push(resultsTable)
return [headingLine1, headingLine2, summary].join('\n\n')
} }
function getSuiteSummary(ts: TestSuiteResult, index: number): string { const suitesReports = testRuns.map((tr, i) => getSuitesReport(tr, i, options)).flat()
sections.push(...suitesReports)
return sections
}
function getSuitesReport(tr: TestRunResult, runIndex: number, options: ReportOptions): string[] {
const sections: string[] = []
const trSlug = makeRunSlug(runIndex)
const nameLink = `<a id="${trSlug.id}" href="${trSlug.link}">${tr.path}</a>`
const icon = getResultIcon(tr.result)
sections.push(`## ${nameLink} ${icon}`)
const time = formatTime(tr.time)
const headingLine2 = `**${tr.tests}** tests were completed in **${time}** with **${tr.passed}** passed, **${tr.failed}** failed and **${tr.skipped}** skipped.`
sections.push(headingLine2)
const suites = options.listSuites === 'failed' ? tr.failedSuites : tr.suites
if (suites.length > 0) {
const suitesTable = table(
['Test suite', 'Passed', 'Failed', 'Skipped', 'Time'],
[Align.Left, Align.Right, Align.Right, Align.Right, Align.Right],
...suites.map((s, suiteIndex) => {
const tsTime = formatTime(s.time)
const tsName = s.name
const skipLink = options.listTests === 'none' || (options.listTests === 'failed' && s.result !== 'failed')
const tsAddr = makeSuiteSlug(runIndex, suiteIndex).link
const tsNameLink = skipLink ? tsName : link(tsName, tsAddr)
const passed = s.passed > 0 ? `${s.passed}${Icon.success}` : ''
const failed = s.failed > 0 ? `${s.failed}${Icon.fail}` : ''
const skipped = s.skipped > 0 ? `${s.skipped}${Icon.skip}` : ''
return [tsNameLink, passed, failed, skipped, tsTime]
})
)
sections.push(suitesTable)
}
if (options.listTests !== 'none') {
const tests = suites.map((ts, suiteIndex) => getTestsReport(ts, runIndex, suiteIndex, options)).flat()
if (tests.length > 1) {
sections.push(...tests)
}
}
return sections
}
function getTestsReport(ts: TestSuiteResult, runIndex: number, suiteIndex: number, options: ReportOptions): string[] {
const groups = options.listTests === 'failed' ? ts.failedGroups : ts.groups
if (groups.length === 0) {
return []
}
const sections: string[] = []
const tsName = ts.name
const tsSlug = makeSuiteSlug(runIndex, suiteIndex)
const tsNameLink = `<a id="${tsSlug.id}" href="${tsSlug.link}">${tsName}</a>`
const icon = getResultIcon(ts.result) const icon = getResultIcon(ts.result)
const content = ts.groups sections.push(`### ${tsNameLink} ${icon}`)
.map(grp => {
const header = grp.name ? `### ${grp.name}\n\n` : '' const tsTime = formatTime(ts.time)
const tests = table( const headingLine2 = `**${ts.tests}** tests were completed in **${tsTime}** with **${ts.passed}** passed, **${ts.failed}** failed and **${ts.skipped}** skipped.`
sections.push(headingLine2)
for (const grp of groups) {
const tests = options.listTests === 'failed' ? grp.failedTests : grp.tests
if (tests.length === 0) {
continue
}
const grpHeader = grp.name ? `\n**${grp.name}**` : ''
const testsTable = table(
['Result', 'Test', 'Time'], ['Result', 'Test', 'Time'],
[Align.Center, Align.Left, Align.Right], [Align.Center, Align.Left, Align.Right],
...grp.tests.map(tc => { ...grp.tests.map(tc => {
const name = tc.name const name = tc.name
const time = `${tc.time}ms` const time = formatTime(tc.time)
const result = getResultIcon(tc.result) const result = getResultIcon(tc.result)
return [result, name, time] return [result, name, time]
}) })
) )
return `${header}${tests}\n` sections.push(grpHeader, testsTable)
})
.join('\n')
const tsName = ts.name
const tsSlug = makeSuiteSlug(index, tsName)
const tsNameLink = `<a id="${tsSlug.id}" href="${tsSlug.link}">${tsName}</a>`
return `## ${tsNameLink} ${icon}\n\n${content}`
} }
function makeSuiteSlug(index: number, name: string): {id: string; link: string} { return sections
// use "ts-$index-" as prefix to avoid slug conflicts after escaping the paths }
return slug(`ts-${index}-${name}`)
function makeRunSlug(runIndex: number): {id: string; link: string} {
// use prefix to avoid slug conflicts after escaping the paths
return slug(`r${runIndex}`)
}
function makeSuiteSlug(runIndex: number, suiteIndex: number): {id: string; link: string} {
// use prefix to avoid slug conflicts after escaping the paths
return slug(`r${runIndex}s${suiteIndex}`)
} }
function getResultIcon(result: TestExecutionResult): string { function getResultIcon(result: TestExecutionResult): string {

11
src/test-parser.ts Normal file
View file

@ -0,0 +1,11 @@
import {TestRunResult} from './test-results'
export interface ParseOptions {
parseErrors: boolean
workDir: string
trackedFiles: string[]
}
export interface TestParser {
parse(path: string, content: string): Promise<TestRunResult>
}

View file

@ -22,6 +22,10 @@ export class TestRunResult {
get result(): TestExecutionResult { get result(): TestExecutionResult {
return this.suites.some(t => t.result === 'failed') ? 'failed' : 'success' return this.suites.some(t => t.result === 'failed') ? 'failed' : 'success'
} }
get failedSuites(): TestSuiteResult[] {
return this.suites.filter(s => s.result === 'failed')
}
} }
export class TestSuiteResult { export class TestSuiteResult {
@ -47,6 +51,10 @@ export class TestSuiteResult {
get result(): TestExecutionResult { get result(): TestExecutionResult {
return this.groups.some(t => t.result === 'failed') ? 'failed' : 'success' return this.groups.some(t => t.result === 'failed') ? 'failed' : 'success'
} }
get failedGroups(): TestGroupResult[] {
return this.groups.filter(grp => grp.result === 'failed')
}
} }
export class TestGroupResult { export class TestGroupResult {
@ -68,10 +76,26 @@ export class TestGroupResult {
get result(): TestExecutionResult { get result(): TestExecutionResult {
return this.tests.some(t => t.result === 'failed') ? 'failed' : 'success' return this.tests.some(t => t.result === 'failed') ? 'failed' : 'success'
} }
get failedTests(): TestCaseResult[] {
return this.tests.filter(tc => tc.result === 'failed')
}
} }
export class TestCaseResult { export class TestCaseResult {
constructor(readonly name: string, readonly result: TestExecutionResult, readonly time: number) {} constructor(
readonly name: string,
readonly result: TestExecutionResult,
readonly time: number,
readonly error?: TestCaseError
) {}
} }
export type TestExecutionResult = 'success' | 'skipped' | 'failed' | undefined export type TestExecutionResult = 'success' | 'skipped' | 'failed' | undefined
export interface TestCaseError {
path?: string
line?: number
message?: string
stackTrace: string
}

View file

@ -11,10 +11,6 @@ export const Icon = {
fail: '❌' // ':x:' fail: '❌' // ':x:'
} }
export function details(summary: string, content: string): string {
return `<details><summary>${summary}</summary>${content}</details>`
}
export function link(title: string, address: string): string { export function link(title: string, address: string): string {
return `[${title}](${address})` return `[${title}](${address})`
} }
@ -34,3 +30,19 @@ export function tableEscape(content: ToString): string {
export function fixEol(text?: string): string { export function fixEol(text?: string): string {
return text?.replace(/\r/g, '') ?? '' return text?.replace(/\r/g, '') ?? ''
} }
export function ellipsis(text: string, maxLength: number): string {
if (text.length <= maxLength) {
return text
}
return text.substr(0, maxLength - 3) + '...'
}
export function formatTime(ms: number): string {
if (ms > 1000) {
return `${(ms / 1000).toFixed(3)}s`
}
return `${Math.round(ms)}ms`
}

20
src/utils/parse-utils.ts Normal file
View file

@ -0,0 +1,20 @@
export function parseNetDuration(str: string): number {
// matches dotnet duration: 00:00:00.0010000
const durationRe = /^(\d\d):(\d\d):(\d\d\.\d+)$/
const durationMatch = str.match(durationRe)
if (durationMatch === null) {
throw new Error(`Invalid format: "${str}" is not NET duration`)
}
const [_, hourStr, minStr, secStr] = durationMatch
return (parseInt(hourStr) * 3600 + parseInt(minStr) * 60 + parseFloat(secStr)) * 1000
}
export function parseIsoDate(str: string): Date {
const isoDateRe = /^\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)$/
if (str === undefined || !isoDateRe.test(str)) {
throw new Error(`Invalid format: "${str}" is not ISO date`)
}
return new Date(str)
}

View file

@ -1,27 +0,0 @@
const isoDateRe = /^\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)$/
// matches dotnet duration: 00:00:00.0010000
const durationRe = /^(\d\d):(\d\d):(\d\d\.\d+)$/
export function parseAttribute(str: string | undefined): string | Date | number | undefined {
if (str === '' || str === undefined) {
return str
}
if (isoDateRe.test(str)) {
return new Date(str)
}
const durationMatch = str.match(durationRe)
if (durationMatch !== null) {
const [_, hourStr, minStr, secStr] = durationMatch
return (parseInt(hourStr) * 3600 + parseInt(minStr) * 60 + parseFloat(secStr)) * 1000
}
const num = parseFloat(str)
if (isNaN(num)) {
return str
}
return num
}