Skip to content

Commit 02ceed0

Browse files
author
Craigory Coppola
committed
feat(core): dotnet test support #20
1 parent 4eae1c6 commit 02ceed0

File tree

10 files changed

+247
-28
lines changed

10 files changed

+247
-28
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
npm-debug.log
3333
yarn-error.log
3434
testem.log
35+
.npmrc
3536
/typings
3637

3738
# System Files

.npmrc.local

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
; Set a new registry for a scoped package
2+
@nx-dotnet:registry=http://localhost:4873

packages/core/src/executors/test/executor.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,33 @@
1+
import { ExecutorContext } from '@nrwl/devkit';
2+
import {
3+
DotNetClient,
4+
dotnetFactory,
5+
dotnetTestFlags,
6+
} from '@nx-dotnet/dotnet';
7+
import {
8+
getExecutedProjectConfiguration,
9+
getProjectFileForNxProject,
10+
} from '@nx-dotnet/utils';
111
import { TestExecutorSchema } from './schema';
212

3-
export default async function runExecutor(options: TestExecutorSchema) {
4-
console.log('Executor ran for Test', options);
13+
export default async function runExecutor(
14+
options: TestExecutorSchema,
15+
context: ExecutorContext,
16+
dotnetClient: DotNetClient = new DotNetClient(dotnetFactory())
17+
) {
18+
const nxProjectConfiguration = getExecutedProjectConfiguration(context);
19+
const projectFilePath = await getProjectFileForNxProject(
20+
nxProjectConfiguration
21+
);
22+
23+
dotnetClient.test(
24+
projectFilePath,
25+
Object.keys(options).map((x) => ({
26+
flag: x as dotnetTestFlags,
27+
value: (options as Record<string, string | boolean>)[x],
28+
}))
29+
);
30+
531
return {
632
success: true,
733
};
Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,34 @@
1-
export interface TestExecutorSchema {} // eslint-disable-line
1+
export interface TestExecutorSchema {
2+
testAdapterPath?: string;
3+
blame?: boolean;
4+
blameCrash?: boolean;
5+
blameCrashDumpType?: string;
6+
blameCrashCollectAlways?: boolean;
7+
blameHang?: boolean;
8+
blameHangDumpType?: string;
9+
blameHangTimeout?: string;
10+
configuration?: string;
11+
collect?: string;
12+
diag?: string;
13+
framework?: string;
14+
filter?: string;
15+
logger?: string;
16+
noBuild?: boolean;
17+
noRestore?: boolean;
18+
output?: string;
19+
resultsDirectory?: string;
20+
runtime?: string;
21+
settings?: string;
22+
listTests?: boolean;
23+
verbosity?:
24+
| 'quiet'
25+
| 'q'
26+
| 'm'
27+
| 'minimal'
28+
| 'n'
29+
| 'normal'
30+
| 'd'
31+
| 'detailed'
32+
| 'diag'
33+
| 'diagnostic';
34+
}

packages/core/src/executors/test/schema.json

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,111 @@
44
"title": "Test executor",
55
"description": "",
66
"type": "object",
7-
"properties": {},
7+
"properties": {
8+
"testAdapterPath": {
9+
"description": "Path to a directory to be searched for additional test adapters. Only .dll files with suffix .TestAdapter.dll are inspected. If not specified, the directory of the test .dll is searched.",
10+
"type": "string"
11+
},
12+
"blame": {
13+
"description": "Runs the tests in blame mode. This option is helpful in isolating problematic tests that cause the test host to crash. When a crash is detected, it creates a sequence file in TestResults/<Guid>/<Guid>_Sequence.xml that captures the order of tests that were run before the crash.",
14+
"type": "boolean",
15+
"default": false
16+
},
17+
"blameCrash": {
18+
"description": "Runs the tests in blame mode and collects a crash dump when the test host exits unexpectedly. This option depends on the version of .NET used, the type of error, and the operating system. For exceptions in managed code, a dump will be automatically collected on .NET 5.0 and later versions. It will generate a dump for testhost or any child process that also ran on .NET 5.0 and crashed. Crashes in native code will not generate a dump. This option works on Windows, macOS, and Linux. Crash dumps in native code, or when using .NET Core 3.1 or earlier versions, can only be collected on Windows, by using Procdump. A directory that contains procdump.exe and procdump64.exe must be in the PATH or PROCDUMP_PATH environment variable. Download the tools. Implies --blame. To collect a crash dump from a native application running on .NET 5.0 or later, the usage of Procdump can be forced by setting the VSTEST_DUMP_FORCEPROCDUMP environment variable to 1.",
19+
"type": "boolean",
20+
"default": false
21+
},
22+
"blameCrashDumpType": {
23+
"description": "The type of crash dump to be collected. Implies --blame-crash.",
24+
"type": "string"
25+
},
26+
"blameCrashCollectAlways": {
27+
"description": "Collects a crash dump on expected as well as unexpected test host exit.",
28+
"type": "boolean",
29+
"default": false
30+
},
31+
"blameHang": {
32+
"description": "Run the tests in blame mode and collects a hang dump when a test exceeds the given timeout.",
33+
"type": "boolean",
34+
"default": false
35+
},
36+
"blameHangDumpType": {
37+
"description": "The type of crash dump to be collected. It should be full, mini, or none. When none is specified, test host is terminated on timeout, but no dump is collected. Implies --blame-hang.",
38+
"type": "string"
39+
},
40+
"blameHangTimeout": {
41+
"description": "Per-test timeout, after which a hang dump is triggered and the test host process and all of its child processes are dumped and terminated. The timeout value is specified in one of the following formats: \n 1.5h, 1.5hour, 1.5hours \n 90m, 90min, 90minute, 90minutes \n 5400s, 5400sec, 5400second, 5400seconds \n 5400000ms, 5400000mil, 5400000millisecond, 5400000milliseconds \n When no unit is used (for example, 5400000), the value is assumed to be in milliseconds. When used together with data driven tests, the timeout behavior depends on the test adapter used. For xUnit and NUnit the timeout is renewed after every test case. For MSTest, the timeout is used for all test cases. This option is supported on Windows with netcoreapp2.1 and later, on Linux with netcoreapp3.1 and later, and on macOS with net5.0 or later. Implies --blame and --blame-hang.",
42+
"type": "string"
43+
},
44+
"configuration": {
45+
"description": "Defines the build configuration. The default value is Debug, but your project's configuration could override this default SDK setting.",
46+
"type": "string"
47+
},
48+
"collect": {
49+
"description": "Enables data collector for the test run. For more information, see Monitor and analyze test run. \n To collect code coverage on any platform that is supported by .NET Core, install Coverlet and use the --collect:\"XPlat Code Coverage\" option. \n On Windows, you can collect code coverage by using the --collect \"Code Coverage\" option. This option generates a .coverage file, which can be opened in Visual Studio 2019 Enterprise. For more information, see Use code coverage and Customize code coverage analysis. ",
50+
"type": "string"
51+
},
52+
"diag": {
53+
"description": "Enables diagnostic mode for the test platform and writes diagnostic messages to the specified file and to files next to it. The process that is logging the messages determines which files are created, such as *.host_<date>.txt for test host log, and *.datacollector_<date>.txt for data collector log.",
54+
"type": "string"
55+
},
56+
"framework": {
57+
"description": "Forces the use of dotnet or .NET Framework test host for the test binaries. This option only determines which type of host to use. The actual framework version to be used is determined by the runtimeconfig.json of the test project. When not specified, the TargetFramework assembly attribute is used to determine the type of host. When that attribute is stripped from the .dll, the .NET Framework host is used.",
58+
"type": "string"
59+
},
60+
"filter": {
61+
"description": "Filters out tests in the current project using the given expression. For more information, see the Filter option details section. For more information and examples on how to use selective unit test filtering, see Running selective unit tests.",
62+
"type": "string"
63+
},
64+
"logger": {
65+
"description": "Specifies a logger for test results. Unlike MSBuild, dotnet test doesn't accept abbreviations: instead of -l \"console;v=d\" use -l \"console;verbosity=detailed\". Specify the parameter multiple times to enable multiple loggers.",
66+
"type": "string"
67+
},
68+
"noBuild": {
69+
"description": "Doesn't build the test project before running it. It also implicitly sets the - --no-restore flag.",
70+
"type": "boolean"
71+
},
72+
"noRestore": {
73+
"description": "Doesn't execute an implicit restore when running the command.",
74+
"type": "boolean"
75+
},
76+
"output": {
77+
"description": "Directory in which to find the binaries to run. If not specified, the default path is ./bin/<configuration>/<framework>/. For projects with multiple target frameworks (via the TargetFrameworks property), you also need to define --framework when you specify this option. dotnet test always runs tests from the output directory. You can use AppDomain.BaseDirectory to consume test assets in the output directory.",
78+
"type": "string"
79+
},
80+
"resultsDirectory": {
81+
"description": "The directory where the test results are going to be placed. If the specified directory doesn't exist, it's created. The default is TestResults in the directory that contains the project file.",
82+
"type": "string"
83+
},
84+
"runtime": {
85+
"description": "The target runtime to test for.",
86+
"type": "string"
87+
},
88+
"settings": {
89+
"description": "The .runsettings file to use for running the tests. The TargetPlatform element (x86|x64) has no effect for dotnet test. To run tests that target x86, install the x86 version of .NET Core. The bitness of the dotnet.exe that is on the path is what will be used for running tests. For more information, see the following resources:",
90+
"type": "string"
91+
},
92+
"listTests": {
93+
"description": "List the discovered tests instead of running the tests.",
94+
"type": "boolean"
95+
},
96+
"verbosity": {
97+
"description": "Sets the verbosity level of the command. For more information, see LoggerVerbosity.",
98+
"type": "string",
99+
"enum": [
100+
"quiet",
101+
"q",
102+
"m",
103+
"minimal",
104+
"n",
105+
"normal",
106+
"d",
107+
"detailed",
108+
"diag",
109+
"diagnostic"
110+
]
111+
}
112+
},
8113
"required": []
9114
}

packages/core/src/generators/utils/generate-project.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -188,17 +188,13 @@ export async function GenerateProject(
188188
sourceRoot: `${normalizedOptions.projectRoot}`,
189189
targets: {
190190
build: GetBuildExecutorConfiguration(normalizedOptions.projectRoot),
191-
...(projectType === 'application' ? { serve: GetServeExecutorConfig() } : {}),
191+
...(projectType === 'application'
192+
? { serve: GetServeExecutorConfig() }
193+
: {}),
192194
},
193195
tags: normalizedOptions.parsedTags,
194196
};
195197

196-
if (options['test-template'] !== 'none') {
197-
projectConfiguration.targets.test = GetTestExecutorConfig(
198-
normalizedOptions.projectName + '-test'
199-
);
200-
}
201-
202198
addProjectConfiguration(
203199
host,
204200
normalizedOptions.projectName,

packages/dotnet/src/lib/core/dotnet.client.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
import { ChildProcess, execSync, spawn } from 'child_process';
22

3-
import { getParameterString } from '@nx-dotnet/utils';
3+
import {
4+
getParameterString,
5+
swapArrayFieldValueUsingMap,
6+
} from '@nx-dotnet/utils';
47

58
import {
69
dotnetBuildOptions,
710
dotnetNewOptions,
811
dotnetRunOptions,
912
dotnetTemplate,
13+
dotnetTestOptions,
14+
testKeyMap,
1015
} from '../models';
1116
import { LoadedCLI } from './dotnet.factory';
1217

@@ -32,6 +37,17 @@ export class DotNetClient {
3237
return spawn(this.cliCommand.command, cmd.split(' '), { stdio: 'inherit' });
3338
}
3439

40+
test(project: string, parameters?: dotnetTestOptions): Buffer {
41+
let cmd = `${this.cliCommand.command} test ${project}`;
42+
if (parameters) {
43+
parameters = swapArrayFieldValueUsingMap(parameters, 'flag', testKeyMap);
44+
const paramString = parameters ? getParameterString(parameters) : '';
45+
cmd = `${cmd} ${paramString}`;
46+
}
47+
console.log(`Executing Command: ${cmd}`);
48+
return this.logAndExecute(cmd);
49+
}
50+
3551
addProjectReference(hostCsProj: string, targetCsProj: string): Buffer {
3652
return this.logAndExecute(
3753
`${this.cliCommand.command} add ${hostCsProj} reference ${targetCsProj}`
Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,36 @@
11
export type dotnetTestFlags =
2-
| 'blame-crash-collect-always'
3-
| 'blame-crash-dump-type'
4-
| 'blame-crash'
5-
| 'blame-hang-dump'
6-
| 'blame-hang-timeout'
7-
| 'blame-hang'
2+
| 'blameCrashCollectAlways'
3+
| 'blameCrashDumpType'
4+
| 'blameCrash'
5+
| 'blameHangDump'
6+
| 'blameHangTimeout'
7+
| 'blameHang'
88
| 'blame'
99
| 'collect'
1010
| 'configuration'
1111
| 'diag'
1212
| 'filter'
1313
| 'framework'
14-
| 'list-tests'
14+
| 'listTests'
1515
| 'logger'
16-
| 'no-build'
17-
| 'no-restore'
16+
| 'noBuild'
17+
| 'noRestore'
1818
| 'nologo'
19-
| 'results-directory'
19+
| 'resultsDirectory'
2020
| 'settings'
21-
| 'test-adapter-path'
21+
| 'testAdapterPath'
2222
| 'verbosity';
23+
24+
export const testKeyMap: Partial<{ [key in dotnetTestFlags]: string }> = {
25+
blameCrashCollectAlways: 'blame-crash-collect-always',
26+
blameCrash: 'blame-crash',
27+
blameCrashDumpType: 'blame-crash-dump-type',
28+
blameHangDump: 'blame-hang-dump',
29+
blameHang: 'blame-hang',
30+
blameHangTimeout: 'blame-hang-timeout',
31+
listTests: 'list-tests',
32+
noBuild: 'no-build',
33+
noRestore: 'no-restore',
34+
resultsDirectory: 'results-directory',
35+
testAdapterPath: 'test-adapter-path',
36+
};

packages/utils/src/lib/args.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,27 @@
11
export function isDryRun(): boolean {
22
return process.argv.some((x) => x === '--dry-run');
33
}
4+
5+
export function swapKeysUsingMap(
6+
object: Record<string, unknown>,
7+
map: Record<string, string>
8+
): Record<string, unknown> {
9+
return Object.fromEntries(
10+
Object.entries(object).map(([key, value]) => [
11+
key in map ? map[key] : key,
12+
value,
13+
])
14+
);
15+
}
16+
17+
export function swapArrayFieldValueUsingMap<T>(
18+
array: T[],
19+
field: keyof T,
20+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
21+
map: any
22+
) {
23+
return array.map((x) => ({
24+
...x,
25+
field: map[x[field]] ?? x[field],
26+
}));
27+
}

packages/utils/src/lib/parameters.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ import { cmdLineParameter } from './models';
77
*/
88
export function getParameterString(parameters: cmdLineParameter[]): string {
99
return parameters.reduce((acc, current) => {
10-
if (typeof current.value === 'boolean' && current.value) {
11-
return acc + `--${current.flag} `;
10+
if (typeof current.value === 'boolean' || !current.value) {
11+
if (current.value) {
12+
return acc + `--${current.flag} `;
13+
} else {
14+
return acc;
15+
}
1216
} else {
13-
return (
14-
acc + `--${current.flag} ` + (current.value ? `${current.value} ` : '')
15-
);
17+
return acc + `--${current.flag} ${current.value} `;
1618
}
1719
}, '');
1820
}

0 commit comments

Comments
 (0)