diff --git a/app/Models/Build.php b/app/Models/Build.php
index a0d8fe0185..d1943953ab 100644
--- a/app/Models/Build.php
+++ b/app/Models/Build.php
@@ -2,6 +2,7 @@
namespace App\Models;
+use App\Utils\TestDisplay;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -400,4 +401,18 @@ public function percentCoverageForPath(string $path): ?float
return ($loctested / $total_lines) * 100;
}
+
+ /**
+ * The number of not-run tests whose details are not "Disabled".
+ */
+ public function notRunTestsWarningCount(): int
+ {
+ return $this->tests()
+ ->where('status', Test::NOTRUN)
+ ->where(static function (Builder $query): void {
+ $query->whereNull('details')
+ ->orWhere('details', '!=', TestDisplay::DISABLED_DETAILS);
+ })
+ ->count();
+ }
}
diff --git a/app/Utils/TestDisplay.php b/app/Utils/TestDisplay.php
new file mode 100644
index 0000000000..f1bb526307
--- /dev/null
+++ b/app/Utils/TestDisplay.php
@@ -0,0 +1,54 @@
+ 'normal',
+ Test::FAILED => 'error',
+ Test::NOTRUN => 'warning',
+ default => '',
+ };
+ }
+
+ public static function statusTextColorClass(string $status, ?string $details): string
+ {
+ return match (self::statusColorClass($status, $details)) {
+ 'normal' => 'normal-text',
+ 'warning' => 'warning-text',
+ 'error' => 'error-text',
+ default => '',
+ };
+ }
+
+ public static function graphqlStatusColorClass(string $status, ?string $details): string
+ {
+ $dbStatus = match ($status) {
+ 'NOT_RUN' => Test::NOTRUN,
+ 'PASSED' => Test::PASSED,
+ 'FAILED' => Test::FAILED,
+ default => strtolower($status),
+ };
+
+ return self::statusColorClass($dbStatus, $details);
+ }
+}
diff --git a/app/cdash/app/Controller/Api/Index.php b/app/cdash/app/Controller/Api/Index.php
index b70f94fd90..ebd70ae7c9 100644
--- a/app/cdash/app/Controller/Api/Index.php
+++ b/app/cdash/app/Controller/Api/Index.php
@@ -17,6 +17,7 @@
namespace CDash\Controller\Api;
+use App\Models\Build as EloquentBuild;
use App\Utils\TestingDay;
use CDash\Database;
use CDash\Model\BuildGroup;
@@ -1098,6 +1099,7 @@ public function generateBuildResponseFromRow(array $build_array): array|false
}
$test_response['notrun'] = $nnotrun;
+ $test_response['notrunwarning'] = $this->computeNotRunTestsWarningCount($buildid, $nnotrun);
$test_response['fail'] = $nfail;
$test_response['pass'] = $npass;
@@ -1555,4 +1557,16 @@ public function recordGenerationTime(array &$response): void
{
$this->pageTimer->end($response);
}
+
+ /**
+ * Count not-run tests that should display as warnings on the index page.
+ */
+ private function computeNotRunTestsWarningCount(int $buildid, int $notRunCount): int
+ {
+ if ($notRunCount <= 0) {
+ return 0;
+ }
+
+ return EloquentBuild::find($buildid)?->notRunTestsWarningCount() ?? 0;
+ }
}
diff --git a/app/cdash/app/Controller/Api/QueryTests.php b/app/cdash/app/Controller/Api/QueryTests.php
index 7f00f75295..e82746126c 100644
--- a/app/cdash/app/Controller/Api/QueryTests.php
+++ b/app/cdash/app/Controller/Api/QueryTests.php
@@ -20,6 +20,7 @@
use App\Models\PinnedTestMeasurement;
use App\Models\Project as EloquentProject;
use App\Models\TestMeasurement;
+use App\Utils\TestDisplay;
use CDash\Database;
use CDash\Model\Build;
use CDash\Model\Project;
@@ -423,7 +424,10 @@ public function getResponse(): array
case 'notrun':
$test['status'] = 'Not Run';
- $test['statusclass'] = 'warning';
+ $test['statusclass'] = TestDisplay::statusColorClass(
+ 'notrun',
+ $row->details,
+ );
break;
}
diff --git a/app/cdash/tests/test_testhistory.php b/app/cdash/tests/test_testhistory.php
index 7f06fb55fc..e9bd2f3a24 100644
--- a/app/cdash/tests/test_testhistory.php
+++ b/app/cdash/tests/test_testhistory.php
@@ -134,6 +134,9 @@ private function generateXML($mm, $timestamp, $include_sporadic, $flaky_passed)
./notrun
+
+ Skipped
+
diff --git a/graphql/schema.graphql b/graphql/schema.graphql
index 1eab0ea949..1ae17527b8 100644
--- a/graphql/schema.graphql
+++ b/graphql/schema.graphql
@@ -856,6 +856,11 @@ type Build {
"The number of tests not run for this build."
notRunTestsCount: Int @rename(attribute: "testnotrun")
+ """
+ The number of not-run tests counted as warnings (excludes tests whose details are "Disabled").
+ """
+ notRunTestsWarningCount: Int! @method(name: "notRunTestsWarningCount")
+
"The duration of the test step in seconds."
testDuration: Int! @rename(attribute: "testduration")
diff --git a/resources/js/angular/views/partials/build.html b/resources/js/angular/views/partials/build.html
index ed5b8dc1a9..ca631fbcd8 100644
--- a/resources/js/angular/views/partials/build.html
+++ b/resources/js/angular/views/partials/build.html
@@ -329,7 +329,7 @@
-|
+ |
diff --git a/resources/js/vue/components/BuildSummary.vue b/resources/js/vue/components/BuildSummary.vue
index 373471eaec..05ff0daa11 100644
--- a/resources/js/vue/components/BuildSummary.vue
+++ b/resources/js/vue/components/BuildSummary.vue
@@ -516,6 +516,7 @@ export default {
buildWarningsCount
failedTestsCount
notRunTestsCount
+ notRunTestsWarningCount
site {
id
name
@@ -539,6 +540,7 @@ export default {
buildWarningsCount
failedTestsCount
notRunTestsCount
+ notRunTestsWarningCount
}
nextBuild: build(id: $nextId) @include(if: $hasNext) {
id
@@ -548,6 +550,7 @@ export default {
buildWarningsCount
failedTestsCount
notRunTestsCount
+ notRunTestsWarningCount
}
}
`,
@@ -576,7 +579,7 @@ export default {
nerrors: Math.max(0, prev.buildErrorsCount),
nwarnings: Math.max(0, prev.buildWarningsCount),
ntestfailed: Math.max(0, prev.failedTestsCount),
- ntestnotrun: Math.max(0, prev.notRunTestsCount),
+ ntestnotrun: Math.max(0, prev.notRunTestsWarningCount),
};
} else {
this.cdash.previousbuild = null;
@@ -591,7 +594,7 @@ export default {
nerrors: Math.max(0, next.buildErrorsCount),
nwarnings: Math.max(0, next.buildWarningsCount),
ntestfailed: Math.max(0, next.failedTestsCount),
- ntestnotrun: Math.max(0, next.notRunTestsCount),
+ ntestnotrun: Math.max(0, next.notRunTestsWarningCount),
};
} else {
this.cdash.nextbuild = null;
@@ -616,7 +619,7 @@ export default {
this.cdash.test = {
nfailed: Math.max(0, build.failedTestsCount),
- nnotrun: Math.max(0, build.notRunTestsCount),
+ nnotrun: Math.max(0, build.notRunTestsWarningCount),
};
this.cdash.projectname_encoded = encodeURIComponent(build.project.name);
diff --git a/resources/js/vue/components/BuildTestsPage.vue b/resources/js/vue/components/BuildTestsPage.vue
index 788c83b846..0484367b46 100644
--- a/resources/js/vue/components/BuildTestsPage.vue
+++ b/resources/js/vue/components/BuildTestsPage.vue
@@ -73,6 +73,7 @@ import LoadingIndicator from './shared/LoadingIndicator.vue';
import BuildSummaryCard from './shared/BuildSummaryCard.vue';
import BuildSidebar from './shared/BuildSidebar.vue';
import { DateTime } from 'luxon';
+import { testStatusToColorClass } from './shared/TestDisplay';
const TEST_QUERY = gql`
query(
@@ -325,14 +326,14 @@ export default {
value: edge.node.status,
text: this.humanReadableTestStatus(edge.node.status),
href: `${this.$baseURL}/tests/${edge.node.id}`,
- classes: [this.testStatusToColorClass(edge.node.status)],
+ classes: [testStatusToColorClass(edge.node.status, edge.node.details)],
},
subProject: edge.subProject ?? '',
timeStatus: {
value: edge.node.timeStatusCategory,
text: this.humanReadableTestStatus(edge.node.timeStatusCategory),
href: `${this.$baseURL}/tests/${edge.node.id}?graph=time`,
- classes: [this.testStatusToColorClass(edge.node.timeStatusCategory)],
+ classes: [testStatusToColorClass(edge.node.timeStatusCategory, edge.node.details)],
},
history: {
value: '',
@@ -349,19 +350,6 @@ export default {
},
methods: {
- testStatusToColorClass(status) {
- switch (status) {
- case 'PASSED':
- return 'normal';
- case 'FAILED':
- return 'error';
- case 'NOT_RUN':
- return 'warning';
- default:
- return '';
- }
- },
-
humanReadableTestStatus(status) {
switch (status) {
case 'PASSED':
diff --git a/resources/js/vue/components/ProjectSettings/GeneralTab.vue b/resources/js/vue/components/ProjectSettings/GeneralTab.vue
index 2e68a274c6..48a6838793 100644
--- a/resources/js/vue/components/ProjectSettings/GeneralTab.vue
+++ b/resources/js/vue/components/ProjectSettings/GeneralTab.vue
@@ -362,7 +362,8 @@
{
+ document.getElementById('Testing')?.scrollIntoView();
+ });
+ },
+ },
};
diff --git a/resources/js/vue/components/TestDetailsPage.vue b/resources/js/vue/components/TestDetailsPage.vue
index 65cfeb8f93..d17854a359 100644
--- a/resources/js/vue/components/TestDetailsPage.vue
+++ b/resources/js/vue/components/TestDetailsPage.vue
@@ -334,6 +334,7 @@ import {
faChartLine,
faLink,
} from '@fortawesome/free-solid-svg-icons';
+import { isAcceptableNotRun } from './shared/TestDisplay';
export default {
name: 'TestDetails',
@@ -517,6 +518,14 @@ export default {
},
testStatusPillClass() {
+ if (!this.test) {
+ return 'tw-bg-neutral tw-text-neutral-content';
+ }
+
+ if (this.test.status === 'NOT_RUN' && isAcceptableNotRun(this.test.details)) {
+ return 'tw-bg-success tw-text-success-content';
+ }
+
switch (this.test.status) {
case 'PASSED':
return 'tw-bg-success tw-text-success-content';
diff --git a/resources/js/vue/components/shared/BuildSummaryCard.vue b/resources/js/vue/components/shared/BuildSummaryCard.vue
index ee46268fe1..5dfe2b74b7 100644
--- a/resources/js/vue/components/shared/BuildSummaryCard.vue
+++ b/resources/js/vue/components/shared/BuildSummaryCard.vue
@@ -348,6 +348,7 @@ export default {
passedTestsCount
failedTestsCount
notRunTestsCount
+ notRunTestsWarningCount
site {
id
name
@@ -455,9 +456,15 @@ export default {
return 'No Submission';
}
+ const notRunWarningCount = Math.max(0, this.build.notRunTestsWarningCount);
+ const notRunSkippedCount = Math.max(0, this.build.notRunTestsCount - notRunWarningCount);
+
let retval = '';
- if (this.build.notRunTestsCount > 0) {
- retval += `${this.build.notRunTestsCount} Not Run${this.commaSeparator(this.build.failedTestsCount > 0 || this.build.passedTestsCount)}`;
+ if (notRunWarningCount > 0) {
+ retval += `${notRunWarningCount} Not Run${this.commaSeparator(this.build.failedTestsCount > 0 || this.build.passedTestsCount > 0 || notRunSkippedCount > 0)}`;
+ }
+ if (notRunSkippedCount > 0) {
+ retval += `${notRunSkippedCount} Skipped${this.commaSeparator(this.build.failedTestsCount > 0 || this.build.passedTestsCount > 0)}`;
}
if (this.build.failedTestsCount > 0) {
retval += `${this.build.failedTestsCount} Failed${this.commaSeparator(this.build.passedTestsCount)}`;
@@ -565,19 +572,26 @@ export default {
testColor() {
if (this.build.failedTestsCount > 0) {
return 'tw-bg-red-400';
- } else {
- return 'tw-bg-green-400';
}
+ if (this.build.notRunTestsWarningCount > 0) {
+ return 'tw-bg-orange-400';
+ }
+
+ return 'tw-bg-green-400';
},
testHighlightColor() {
if (!this.hasTest) {
return 'tw-border-x-gray-400';
- } else if (this.build.failedTestsCount > 0) {
+ }
+ if (this.build.failedTestsCount > 0) {
return 'tw-border-x-red-400';
- } else {
- return 'tw-border-x-green-400';
}
+ if (this.build.notRunTestsWarningCount > 0) {
+ return 'tw-border-x-orange-400';
+ }
+
+ return 'tw-border-x-green-400';
},
/**
diff --git a/resources/js/vue/components/shared/FormSection.vue b/resources/js/vue/components/shared/FormSection.vue
index c95a864cb6..3061ef9487 100644
--- a/resources/js/vue/components/shared/FormSection.vue
+++ b/resources/js/vue/components/shared/FormSection.vue
@@ -1,5 +1,8 @@
-
+
{{ title }}
@@ -18,6 +21,12 @@ export default {
type: String,
required: true,
},
+
+ sectionId: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
};
diff --git a/resources/js/vue/components/shared/TestDisplay.js b/resources/js/vue/components/shared/TestDisplay.js
new file mode 100644
index 0000000000..584f277e44
--- /dev/null
+++ b/resources/js/vue/components/shared/TestDisplay.js
@@ -0,0 +1,50 @@
+/**
+ * CTest sets Details="Disabled" for tests marked DISABLED.
+ */
+export const DISABLED_DETAILS = 'Disabled';
+
+/**
+ * @param {string|null|undefined} details
+ * @returns {boolean}
+ */
+export function isAcceptableNotRun(details) {
+ return details === DISABLED_DETAILS;
+}
+
+/**
+ * @param {string} status GraphQL TestStatus enum value.
+ * @param {string} details
+ */
+export function testStatusToColorClass(status, details = '') {
+ if (status === 'NOT_RUN' && isAcceptableNotRun(details)) {
+ return 'normal';
+ }
+
+ switch (status) {
+ case 'PASSED':
+ return 'normal';
+ case 'FAILED':
+ return 'error';
+ case 'NOT_RUN':
+ return 'warning';
+ default:
+ return '';
+ }
+}
+
+/**
+ * @param {string} status
+ * @param {string} details
+ */
+export function testStatusToTextColorClass(status, details = '') {
+ switch (testStatusToColorClass(status, details)) {
+ case 'normal':
+ return 'normal-text';
+ case 'warning':
+ return 'warning-text';
+ case 'error':
+ return 'error-text';
+ default:
+ return '';
+ }
+}
diff --git a/tests/Browser/Pages/BuildTestsPageTest.php b/tests/Browser/Pages/BuildTestsPageTest.php
index 3f4c009246..4f09cc8a19 100644
--- a/tests/Browser/Pages/BuildTestsPageTest.php
+++ b/tests/Browser/Pages/BuildTestsPageTest.php
@@ -521,4 +521,47 @@ public function testOnlyDeltaNoResults(): void
;
});
}
+
+ public function testNotRunWithDisabledDetailsShownInGreen(): void
+ {
+ /** @var Build $build */
+ $build = $this->project->builds()->create([
+ 'siteid' => $this->site->id,
+ 'name' => Str::uuid()->toString(),
+ 'uuid' => Str::uuid()->toString(),
+ ]);
+
+ /** @var Test $disabled_not_run_test */
+ $disabled_not_run_test = $build->tests()->create([
+ 'testname' => Str::uuid()->toString(),
+ 'status' => 'notrun',
+ 'details' => 'Disabled',
+ 'outputid' => $this->testOutput->id,
+ ]);
+
+ /** @var Test $warning_not_run_test */
+ $warning_not_run_test = $build->tests()->create([
+ 'testname' => Str::uuid()->toString(),
+ 'status' => 'notrun',
+ 'details' => 'Unable to find executable',
+ 'outputid' => $this->testOutput->id,
+ ]);
+
+ $this->browse(function (Browser $browser) use ($disabled_not_run_test, $warning_not_run_test, $build): void {
+ $browser->visit("/builds/{$build->id}/tests")
+ ->waitFor('@tests-table')
+ ->waitForText($disabled_not_run_test->testname)
+ ;
+
+ self::assertTrue($browser->script(
+ 'return document.querySelector(\'a[href*="/tests/' . $disabled_not_run_test->id . '"]\')'
+ . '?.closest("tr")?.querySelector("td.normal") !== null',
+ )[0]);
+
+ self::assertTrue($browser->script(
+ 'return document.querySelector(\'a[href*="/tests/' . $warning_not_run_test->id . '"]\')'
+ . '?.closest("tr")?.querySelector("td.warning") !== null',
+ )[0]);
+ });
+ }
}
diff --git a/tests/Feature/GraphQL/BuildTypeTest.php b/tests/Feature/GraphQL/BuildTypeTest.php
index 5f5aa3acaf..96f437b2e5 100644
--- a/tests/Feature/GraphQL/BuildTypeTest.php
+++ b/tests/Feature/GraphQL/BuildTypeTest.php
@@ -9,6 +9,7 @@
use App\Models\Label;
use App\Models\Project;
use App\Models\Target;
+use App\Models\TestOutput;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
@@ -651,4 +652,54 @@ public function testPercentCoverageForPath(string $path, ?float $expected_percen
],
]);
}
+
+ public function testNotRunTestsWarningCountExcludesDisabledDetails(): void
+ {
+ $output = TestOutput::create([
+ 'path' => 'a',
+ 'command' => 'b',
+ 'output' => 'c',
+ ]);
+
+ /** @var Build $build */
+ $build = $this->project->builds()->create([
+ 'name' => 'build-with-disabled-tests',
+ 'uuid' => Str::uuid()->toString(),
+ 'testnotrun' => 2,
+ ]);
+
+ $build->tests()->create([
+ 'testname' => 'disabled_test',
+ 'status' => 'notrun',
+ 'details' => 'Disabled',
+ 'outputid' => $output->id,
+ ]);
+
+ $build->tests()->create([
+ 'testname' => 'missing_test',
+ 'status' => 'notrun',
+ 'details' => 'Unable to find executable',
+ 'outputid' => $output->id,
+ ]);
+
+ $this->graphQL('
+ query build($id: ID!) {
+ build(id: $id) {
+ notRunTestsCount
+ notRunTestsWarningCount
+ }
+ }
+ ', [
+ 'id' => $build->id,
+ ])->assertExactJson([
+ 'data' => [
+ 'build' => [
+ 'notRunTestsCount' => 2,
+ 'notRunTestsWarningCount' => 1,
+ ],
+ ],
+ ]);
+
+ $output->delete();
+ }
}
diff --git a/tests/Unit/Utils/TestDisplayTest.php b/tests/Unit/Utils/TestDisplayTest.php
new file mode 100644
index 0000000000..c01b9dc196
--- /dev/null
+++ b/tests/Unit/Utils/TestDisplayTest.php
@@ -0,0 +1,60 @@
+
+ */
+ public static function isAcceptableNotRunCases(): array
+ {
+ return [
+ ['Disabled', true],
+ ['disabled', false],
+ ['DISABLED', false],
+ ['Test Disabled', false],
+ ['Disabled test', false],
+ ['Skipped', false],
+ ['', false],
+ [null, false],
+ ];
+ }
+
+ #[DataProvider('statusColorClassCases')]
+ public function testStatusColorClass(
+ string $status,
+ ?string $details,
+ string $expected,
+ ): void {
+ self::assertSame(
+ $expected,
+ TestDisplay::statusColorClass($status, $details),
+ );
+ }
+
+ /**
+ * @return list
+ */
+ public static function statusColorClassCases(): array
+ {
+ return [
+ [Test::PASSED, null, 'normal'],
+ [Test::FAILED, null, 'error'],
+ [Test::NOTRUN, 'Some reason', 'warning'],
+ [Test::NOTRUN, 'Disabled', 'normal'],
+ [Test::NOTRUN, 'skipped', 'warning'],
+ ];
+ }
+}
|