mirror of
https://github.com/game-ci/unity-builder.git
synced 2026-06-12 17:03:55 -07:00
fix(testing): use async exec for parallel test group execution
Replace execSync with promisified exec so Promise.all actually runs test groups in parallel. Add native timeout support via exec options. Add 50MB maxBuffer for large Unity output. Fix ESLint violations (variable naming, padding lines, array push consolidation). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+49
-51
@@ -10416,11 +10416,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||
exports.TestWorkflowService = void 0;
|
||||
const node_child_process_1 = __nccwpck_require__(17718);
|
||||
const node_util_1 = __nccwpck_require__(47261);
|
||||
const node_path_1 = __importDefault(__nccwpck_require__(49411));
|
||||
const core = __importStar(__nccwpck_require__(42186));
|
||||
const test_suite_parser_1 = __nccwpck_require__(92105);
|
||||
const taxonomy_filter_service_1 = __nccwpck_require__(9983);
|
||||
const test_result_reporter_1 = __nccwpck_require__(25147);
|
||||
const execAsync = (0, node_util_1.promisify)(node_child_process_1.exec);
|
||||
/**
|
||||
* Main entry point for the test workflow engine.
|
||||
* Orchestrates parsing of YAML suite definitions, resolving run order,
|
||||
@@ -10433,7 +10435,7 @@ class TestWorkflowService {
|
||||
* group sequentially (runs within a group execute concurrently), and
|
||||
* collects all results.
|
||||
*/
|
||||
static async executeTestSuite(suitePath, params) {
|
||||
static async executeTestSuite(suitePath, parameters) {
|
||||
core.info(`[TestWorkflow] Loading test suite from: ${suitePath}`);
|
||||
const suite = test_suite_parser_1.TestSuiteParser.parseSuiteFile(suitePath);
|
||||
core.info(`[TestWorkflow] Suite '${suite.name}' loaded with ${suite.runs.length} run(s)`);
|
||||
@@ -10449,7 +10451,7 @@ class TestWorkflowService {
|
||||
const runNames = group.map((r) => r.name).join(', ');
|
||||
core.info(`[TestWorkflow] Executing group ${groupIndex}/${groups.length}: [${runNames}]`);
|
||||
// Execute runs within a group concurrently
|
||||
const groupResults = await Promise.all(group.map((run) => TestWorkflowService.executeTestRun(run, params)));
|
||||
const groupResults = await Promise.all(group.map((run) => TestWorkflowService.executeTestRun(run, parameters)));
|
||||
allResults.push(...groupResults);
|
||||
// Check for failures -- if any run in this group failed, log a warning
|
||||
// but continue to the next group (fail-forward for maximum feedback)
|
||||
@@ -10463,8 +10465,8 @@ class TestWorkflowService {
|
||||
const summary = test_result_reporter_1.TestResultReporter.generateSummary(allResults);
|
||||
core.info(summary);
|
||||
// Write results if output path is configured
|
||||
const resultPath = params.testResultPath;
|
||||
const resultFormat = params.testResultFormat;
|
||||
const resultPath = parameters.testResultPath;
|
||||
const resultFormat = parameters.testResultFormat;
|
||||
if (resultPath) {
|
||||
test_result_reporter_1.TestResultReporter.writeResults(allResults, resultPath, resultFormat);
|
||||
core.info(`[TestWorkflow] Results written to: ${resultPath}`);
|
||||
@@ -10474,27 +10476,30 @@ class TestWorkflowService {
|
||||
/**
|
||||
* Execute a single test run definition.
|
||||
* Builds the Unity CLI arguments based on the run configuration (edit mode,
|
||||
* play mode, built client) and taxonomy filters, executes the command, and
|
||||
* parses the result output.
|
||||
* play mode, built client) and taxonomy filters, executes the command
|
||||
* asynchronously, and parses the result output.
|
||||
*
|
||||
* Uses promisified exec instead of execSync so that Promise.all can
|
||||
* actually run multiple test groups in parallel without blocking the
|
||||
* Node.js event loop.
|
||||
*/
|
||||
static async executeTestRun(run, params) {
|
||||
static async executeTestRun(run, parameters) {
|
||||
core.info(`[TestWorkflow] Starting run: '${run.name}'`);
|
||||
const args = TestWorkflowService.buildUnityArgs(run, params);
|
||||
const unityArguments = TestWorkflowService.buildUnityArgs(run, parameters);
|
||||
const timeoutMs = (run.timeout ?? 600) * 1000;
|
||||
core.info(`[TestWorkflow] Unity args: ${args}`);
|
||||
core.info(`[TestWorkflow] Unity args: ${unityArguments}`);
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
const resultDir = node_path_1.default.join(params.testResultPath ?? './test-results', run.name);
|
||||
const resultFile = node_path_1.default.join(resultDir, 'results.xml');
|
||||
const resultDirectory = node_path_1.default.join(parameters.testResultPath ?? './test-results', run.name);
|
||||
const resultFile = node_path_1.default.join(resultDirectory, 'results.xml');
|
||||
// Build the full Unity command
|
||||
const unityPath = TestWorkflowService.resolveUnityPath(params);
|
||||
const command = `"${unityPath}" ${args} -testResults "${resultFile}"`;
|
||||
const unityPath = TestWorkflowService.resolveUnityPath(parameters);
|
||||
const command = `"${unityPath}" ${unityArguments} -testResults "${resultFile}"`;
|
||||
core.info(`[TestWorkflow] Executing: ${command}`);
|
||||
(0, node_child_process_1.execSync)(command, {
|
||||
await execAsync(command, {
|
||||
timeout: timeoutMs,
|
||||
stdio: 'pipe',
|
||||
encoding: 'utf8',
|
||||
cwd: params.projectPath || process.cwd(),
|
||||
maxBuffer: 50 * 1024 * 1024,
|
||||
cwd: parameters.projectPath || process.cwd(),
|
||||
});
|
||||
const duration = (Date.now() - startTime) / 1000;
|
||||
// Parse the result file
|
||||
@@ -10519,16 +10524,18 @@ class TestWorkflowService {
|
||||
}
|
||||
catch (error) {
|
||||
const duration = (Date.now() - startTime) / 1000;
|
||||
const isTimeout = error.killed || error.signal === 'SIGTERM';
|
||||
// The promisified exec sets error.killed when the process is terminated
|
||||
// due to timeout, and error.signal will be 'SIGTERM'
|
||||
const isTimeout = error.killed === true || error.signal === 'SIGTERM';
|
||||
if (isTimeout) {
|
||||
core.error(`[TestWorkflow] Run '${run.name}' timed out after ${run.timeout}s`);
|
||||
core.error(`[TestWorkflow] Run '${run.name}' timed out after ${run.timeout ?? 600}s`);
|
||||
}
|
||||
else {
|
||||
core.error(`[TestWorkflow] Run '${run.name}' failed: ${error.message}`);
|
||||
}
|
||||
// Try to parse partial results even on failure
|
||||
const resultDir = node_path_1.default.join(params.testResultPath ?? './test-results', run.name);
|
||||
const resultFile = node_path_1.default.join(resultDir, 'results.xml');
|
||||
const resultDirectory = node_path_1.default.join(parameters.testResultPath ?? './test-results', run.name);
|
||||
const resultFile = node_path_1.default.join(resultDirectory, 'results.xml');
|
||||
try {
|
||||
const result = test_result_reporter_1.TestResultReporter.parseJUnitResults(resultFile);
|
||||
result.runName = run.name;
|
||||
@@ -10547,7 +10554,7 @@ class TestWorkflowService {
|
||||
testName: isTimeout ? 'Timeout' : 'ExecutionError',
|
||||
className: run.name,
|
||||
message: isTimeout
|
||||
? `Test run timed out after ${run.timeout}s`
|
||||
? `Test run timed out after ${run.timeout ?? 600}s`
|
||||
: error.message ?? 'Unknown execution error',
|
||||
stackTrace: error.stderr ?? undefined,
|
||||
},
|
||||
@@ -10559,65 +10566,56 @@ class TestWorkflowService {
|
||||
/**
|
||||
* Build Unity CLI arguments for a test run based on its configuration.
|
||||
*/
|
||||
static buildUnityArgs(run, params) {
|
||||
const args = [];
|
||||
// Batch mode and no-graphics for CI
|
||||
args.push('-batchmode');
|
||||
args.push('-nographics');
|
||||
static buildUnityArgs(run, parameters) {
|
||||
const unityArguments = ['-batchmode', '-nographics'];
|
||||
// Project path
|
||||
if (params.projectPath) {
|
||||
args.push(`-projectPath "${params.projectPath}"`);
|
||||
if (parameters.projectPath) {
|
||||
unityArguments.push(`-projectPath "${parameters.projectPath}"`);
|
||||
}
|
||||
// Test mode
|
||||
if (run.builtClient && run.builtClientPath) {
|
||||
// Built client testing: run tests against a built player
|
||||
args.push('-runTests');
|
||||
args.push(`-testPlatform StandalonePlayer`);
|
||||
args.push(`-assemblyNames Assembly-CSharp-Tests`);
|
||||
args.push(`-builtPlayerPath "${run.builtClientPath}"`);
|
||||
unityArguments.push('-runTests', `-testPlatform StandalonePlayer`, `-assemblyNames Assembly-CSharp-Tests`, `-builtPlayerPath "${run.builtClientPath}"`);
|
||||
}
|
||||
else if (run.editMode && run.playMode) {
|
||||
// Both modes: run EditMode first, then PlayMode will require a separate invocation
|
||||
// For combined mode, use EditMode (the service handles sequencing)
|
||||
args.push('-runTests');
|
||||
args.push('-testPlatform EditMode');
|
||||
unityArguments.push('-runTests', '-testPlatform EditMode');
|
||||
}
|
||||
else if (run.playMode) {
|
||||
args.push('-runTests');
|
||||
args.push('-testPlatform PlayMode');
|
||||
unityArguments.push('-runTests', '-testPlatform PlayMode');
|
||||
}
|
||||
else if (run.editMode) {
|
||||
args.push('-runTests');
|
||||
args.push('-testPlatform EditMode');
|
||||
unityArguments.push('-runTests', '-testPlatform EditMode');
|
||||
}
|
||||
// Apply taxonomy filters
|
||||
if (run.filters && Object.keys(run.filters).length > 0) {
|
||||
const filterArgs = taxonomy_filter_service_1.TaxonomyFilterService.buildFilterArgs(run.filters);
|
||||
if (filterArgs) {
|
||||
args.push(filterArgs);
|
||||
const filterArguments = taxonomy_filter_service_1.TaxonomyFilterService.buildFilterArgs(run.filters);
|
||||
if (filterArguments) {
|
||||
unityArguments.push(filterArguments);
|
||||
}
|
||||
}
|
||||
// Target platform
|
||||
if (params.targetPlatform) {
|
||||
args.push(`-buildTarget ${params.targetPlatform}`);
|
||||
if (parameters.targetPlatform) {
|
||||
unityArguments.push(`-buildTarget ${parameters.targetPlatform}`);
|
||||
}
|
||||
return args.join(' ');
|
||||
return unityArguments.join(' ');
|
||||
}
|
||||
/**
|
||||
* Resolve the path to the Unity editor executable.
|
||||
*/
|
||||
static resolveUnityPath(params) {
|
||||
static resolveUnityPath(parameters) {
|
||||
// In CI, Unity path is typically set via environment or the docker container
|
||||
const envUnityPath = process.env.UNITY_PATH ?? process.env.UNITY_EDITOR;
|
||||
if (envUnityPath) {
|
||||
return envUnityPath;
|
||||
const environmentUnityPath = process.env.UNITY_PATH ?? process.env.UNITY_EDITOR;
|
||||
if (environmentUnityPath) {
|
||||
return environmentUnityPath;
|
||||
}
|
||||
// Default paths by platform
|
||||
if (process.platform === 'win32') {
|
||||
return `C:/Program Files/Unity/Hub/Editor/${params.editorVersion}/Editor/Unity.exe`;
|
||||
return `C:/Program Files/Unity/Hub/Editor/${parameters.editorVersion}/Editor/Unity.exe`;
|
||||
}
|
||||
if (process.platform === 'darwin') {
|
||||
return `/Applications/Unity/Hub/Editor/${params.editorVersion}/Unity.app/Contents/MacOS/Unity`;
|
||||
return `/Applications/Unity/Hub/Editor/${parameters.editorVersion}/Unity.app/Contents/MacOS/Unity`;
|
||||
}
|
||||
// Linux default (Docker container path)
|
||||
return '/opt/unity/Editor/Unity';
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user