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 @@