diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 89995be..ac4db79 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -67,6 +67,13 @@ on: type: number required: false default: 4 + rust-test-threads: + description: >- + Optional positive Rust test-harness thread count for the single-job cargo llvm-cov path + (shards <= 1). Empty keeps the test harness default; ignored by sharded nextest runs. + type: string + required: false + default: "" secrets: SONAR_TOKEN: description: "Legacy or repository-level SonarCloud analysis token" @@ -670,13 +677,19 @@ jobs: CARGO_PROFILE_DEV_DEBUG: "0" SQLX_OFFLINE_INPUT: ${{ inputs.sqlx-offline }} CARGO_FEATURES: ${{ inputs.cargo-features }} + RUST_TEST_THREADS_INPUT: ${{ inputs.rust-test-threads }} run: | # Opt-in build settings; validated because they reach the cargo command line. if [[ ! "$CARGO_FEATURES" =~ ^[A-Za-z0-9_/,-]*$ ]]; then echo "::error::cargo-features may only hold feature names, '/', '-', '_' and ',', got '$CARGO_FEATURES'" exit 1 fi + if [[ -n "$RUST_TEST_THREADS_INPUT" && ! "$RUST_TEST_THREADS_INPUT" =~ ^[1-9][0-9]*$ ]]; then + echo "::error::rust-test-threads must be a positive integer, got '$RUST_TEST_THREADS_INPUT'" + exit 1 + fi if [ "$SQLX_OFFLINE_INPUT" = true ]; then export SQLX_OFFLINE=true; fi + if [ -n "$RUST_TEST_THREADS_INPUT" ]; then export RUST_TEST_THREADS="$RUST_TEST_THREADS_INPUT"; fi features=() if [ -n "$CARGO_FEATURES" ]; then features=(--features "$CARGO_FEATURES"); fi # Run every instrumented test and remember failures, but still create coverage so diff --git a/tests/test_sonarcloud.py b/tests/test_sonarcloud.py index a62f82c..4ad1d28 100644 --- a/tests/test_sonarcloud.py +++ b/tests/test_sonarcloud.py @@ -174,7 +174,8 @@ class SonarCloudTests(unittest.TestCase): cwd=root, env={**os.environ, **coverage.get('env', {}), 'PATH': f'{bin_dir}:{os.environ["PATH"]}', 'CARGO_CALLS': str(calls), 'GITHUB_OUTPUT': str(output), - 'CARGO_FEATURES': '', 'SQLX_OFFLINE_INPUT': 'false', 'COVERAGE_IGNORE': ''}, + 'CARGO_FEATURES': '', 'SQLX_OFFLINE_INPUT': 'false', + 'RUST_TEST_THREADS_INPUT': '', 'COVERAGE_IGNORE': ''}, capture_output=True, text=True, ) @@ -186,6 +187,58 @@ class SonarCloudTests(unittest.TestCase): self.assertIn('--no-report --no-fail-fast', invocations[0]) self.assertEqual(invocations[1], 'llvm-cov report --lcov --output-path lcov.info --ignore-filename-regex ') + def test_rust_test_threads_input_is_validated_and_exported(self): + coverage = next(step for step in self.steps if step.get('id') == 'coverage') + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + bin_dir = root / 'bin' + bin_dir.mkdir() + cargo = bin_dir / 'cargo' + cargo.write_text( + '#!/usr/bin/env bash\n' + 'printf "%s\\n" "${RUST_TEST_THREADS-unset}" >> "$THREADS_CALLS"\n' + 'if [[ "$*" == *"--no-report"* ]]; then exit 0; fi\n' + 'printf "TN:\\n" > lcov.info\n' + ) + cargo.chmod(0o755) + calls = root / 'threads' + result = subprocess.run( + ['bash', '--noprofile', '--norc', '-e', '-o', 'pipefail', '-c', coverage['run']], + cwd=root, + env={**os.environ, **coverage.get('env', {}), 'PATH': f'{bin_dir}:{os.environ["PATH"]}', + 'THREADS_CALLS': str(calls), 'GITHUB_OUTPUT': str(root / 'output'), + 'CARGO_FEATURES': '', 'SQLX_OFFLINE_INPUT': 'false', + 'RUST_TEST_THREADS_INPUT': '1', 'COVERAGE_IGNORE': ''}, + capture_output=True, + text=True, + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(calls.read_text().splitlines(), ['1', '1']) + + def test_invalid_rust_test_threads_cannot_start_coverage(self): + coverage = next(step for step in self.steps if step.get('id') == 'coverage') + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + bin_dir = root / 'bin' + bin_dir.mkdir() + cargo = bin_dir / 'cargo' + cargo.write_text('#!/usr/bin/env bash\nprintf called >> "$CARGO_CALLS"\n') + cargo.chmod(0o755) + calls = root / 'calls' + result = subprocess.run( + ['bash', '--noprofile', '--norc', '-e', '-o', 'pipefail', '-c', coverage['run']], + cwd=root, + env={**os.environ, **coverage.get('env', {}), 'PATH': f'{bin_dir}:{os.environ["PATH"]}', + 'CARGO_CALLS': str(calls), 'GITHUB_OUTPUT': str(root / 'output'), + 'CARGO_FEATURES': '', 'SQLX_OFFLINE_INPUT': 'false', + 'RUST_TEST_THREADS_INPUT': '0', 'COVERAGE_IGNORE': ''}, + capture_output=True, + text=True, + ) + self.assertNotEqual(result.returncode, 0) + self.assertIn('rust-test-threads must be a positive integer', result.stdout) + self.assertFalse(calls.exists()) + def test_coverage_generation_failure_is_blocking_after_scan(self): coverage = next(step for step in self.steps if step.get('id') == 'coverage') failure = next(step for step in self.steps