From 4b484f9a1b0af534f1861ce6ae538957cdba4028 Mon Sep 17 00:00:00 2001 From: OmniLab Team Date: Mon, 20 Jul 2026 20:13:01 -0700 Subject: [PATCH] Internal change PiperOrigin-RevId: 951203692 --- .../mobileharness/fe/v6/angular/app/app.ts | 6 + .../fe/v6/angular/app/app_routes.ts | 24 ++- .../core/interceptors/universe_interceptor.ts | 7 +- .../angular/app/core/models/common_models.ts | 103 +++++++++++++ .../angular/app/core/models/test_overview.ts | 139 ++++-------------- .../mock_data/tests/overview_error.ts | 2 +- .../mock_data/tests/overview_failed.ts | 2 +- .../mock_data/tests/overview_inprogress.ts | 2 +- .../mock_data/tests/overview_longname.ts | 2 +- .../overview_multiple_errors_warnings.ts | 2 +- .../mock_data/tests/overview_passed.ts | 2 +- .../mock_data/tests/overview_skipped.ts | 2 +- .../mock_data/tests/overview_suspended.ts | 2 +- .../mock_data/tests/overview_timeout.ts | 2 +- .../mock_data/tests/overview_warning.ts | 2 +- .../core/services/test/fake_test_service.ts | 97 ++++++++---- .../core/services/test/http_test_service.ts | 12 +- .../fe/v6/angular/app/core/utils/url_utils.ts | 14 ++ .../dev_harness/dev_harness_page.ng.html | 2 +- .../test_log_tab/test_log_tab.ng.html | 4 +- .../test_log_tab/test_log_tab.spec.ts | 13 +- .../components/test_log_tab/test_log_tab.ts | 76 +++++----- .../test_overview_tab.ng.html | 9 +- .../test_overview_tab/test_overview_tab.scss | 60 ++------ .../test_overview_tab.spec.ts | 2 +- .../test_overview_tab/test_overview_tab.ts | 26 ++-- .../test_detail/models/test_page_ui.ts | 1 + .../features/test_detail/test_detail.ng.html | 30 ++-- .../features/test_detail/test_detail.spec.ts | 5 +- .../app/features/test_detail/test_detail.ts | 16 +- .../accordion_item/accordion_item.scss | 1 + .../fe/v6/service/proto/test/BUILD | 1 + .../v6/service/proto/test/test_service.proto | 15 +- .../devtools/mobileharness/fe/v6/server/BUILD | 1 + .../fe/v6/server/OssFeServer.java | 7 + .../mobileharness/fe/v6/service/test/BUILD | 35 +++++ .../v6/service/test/NoOpTestServiceLogic.java | 41 ++++++ .../v6/service/test/OssTestServiceModule.java | 27 ++++ .../v6/service/test/TestServiceGrpcImpl.java | 63 ++++++++ .../fe/v6/service/test/TestServiceLogic.java | 33 +++++ .../devtools/mobileharness/fe/v6/server/BUILD | 1 + .../fe/v6/server/OssFeServerTest.java | 2 + 42 files changed, 600 insertions(+), 293 deletions(-) create mode 100644 src/devtools/mobileharness/fe/v6/angular/app/core/models/common_models.ts create mode 100644 src/java/com/google/devtools/mobileharness/fe/v6/service/test/BUILD create mode 100644 src/java/com/google/devtools/mobileharness/fe/v6/service/test/NoOpTestServiceLogic.java create mode 100644 src/java/com/google/devtools/mobileharness/fe/v6/service/test/OssTestServiceModule.java create mode 100644 src/java/com/google/devtools/mobileharness/fe/v6/service/test/TestServiceGrpcImpl.java create mode 100644 src/java/com/google/devtools/mobileharness/fe/v6/service/test/TestServiceLogic.java diff --git a/src/devtools/mobileharness/fe/v6/angular/app/app.ts b/src/devtools/mobileharness/fe/v6/angular/app/app.ts index 7c645ce0c4..f49a4aa3d6 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/app.ts +++ b/src/devtools/mobileharness/fe/v6/angular/app/app.ts @@ -133,6 +133,12 @@ export class App implements OnDestroy { this.showContent = true; } else if (path === 'hosts/:hostName' && params['hostName']) { this.showContent = true; + } else if ( + path === 'jobs/:jobId/tests/:id' && + params['id'] && + params['jobId'] + ) { + this.showContent = true; } else { this.showContent = false; } diff --git a/src/devtools/mobileharness/fe/v6/angular/app/app_routes.ts b/src/devtools/mobileharness/fe/v6/angular/app/app_routes.ts index 272faed563..02c017e70e 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/app_routes.ts +++ b/src/devtools/mobileharness/fe/v6/angular/app/app_routes.ts @@ -1,9 +1,28 @@ -import {Routes} from '@angular/router'; +import {inject} from '@angular/core'; +import {CanActivateFn, Router, Routes} from '@angular/router'; import {DevHarnessPage} from './features/dev_harness/dev_harness_page'; import {DeviceDetailPage} from './features/device_detail/device_detail_page'; import {HostDetail} from './features/host_detail/host_detail'; import {TestDetail} from './features/test_detail/test_detail'; +/** + * Router Activation Guard (Sanitizes the browser address bar on initialization or external entrance). + * If a user enters a Test/Job detail page from outside the guest app (e.g. bookmarks or direct links) + * with '?universe=xxx' query param, this guard intercepts the navigation, strips universe from UrlTree, + * and performs a clean redirection. + * Note: This gates external/initial entries, while url_utils manages internal nav link transitions, + * and universeInterceptor shields the backend HTTP request layer. + */ +const stripUniverseGuard: CanActivateFn = (route, state) => { + const router = inject(Router); + if (route.queryParamMap.has('universe')) { + const urlTree = router.parseUrl(state.url); + delete urlTree.queryParams['universe']; + return urlTree; + } + return true; +}; + /** * The application routes. */ @@ -21,8 +40,9 @@ export const routes: Routes = [ component: HostDetail, }, { - path: 'tests/:id', + path: 'jobs/:jobId/tests/:id', component: TestDetail, + canActivate: [stripUniverseGuard], }, { path: '', diff --git a/src/devtools/mobileharness/fe/v6/angular/app/core/interceptors/universe_interceptor.ts b/src/devtools/mobileharness/fe/v6/angular/app/core/interceptors/universe_interceptor.ts index 4f3a0e15d5..2f2389d463 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/core/interceptors/universe_interceptor.ts +++ b/src/devtools/mobileharness/fe/v6/angular/app/core/interceptors/universe_interceptor.ts @@ -5,7 +5,12 @@ export const universeInterceptor: HttpInterceptorFn = (req, next) => { const urlParams = new URLSearchParams(window.location.search); const universe = urlParams.get('universe'); - if (universe) { + // NETWORK LAYER INTERCEPTION: Exclude '/tests' and '/jobs' requests. + // The gRPC backends for test/job detail (e.g. GetTestRequest) do not support the 'universe' field. + // Injecting it would trigger a backend GFEs / HTTP-transcoding 400 Bad Request error. + // Note: This operates at the HTTP/network request level, while stripUniverseGuard and url_utils + // sanitize the browser navigation URL bar parameters to keep the addresses clean. + if (universe && !req.url.includes('/tests') && !req.url.includes('/jobs')) { const patchedReq = req.clone({ setParams: { 'universe': universe, diff --git a/src/devtools/mobileharness/fe/v6/angular/app/core/models/common_models.ts b/src/devtools/mobileharness/fe/v6/angular/app/core/models/common_models.ts new file mode 100644 index 0000000000..40c91e9b34 --- /dev/null +++ b/src/devtools/mobileharness/fe/v6/angular/app/core/models/common_models.ts @@ -0,0 +1,103 @@ +/** + * @fileoverview Shared core data models and enums mapping to MobileHarness protos. + */ + +/** gRPC-friendly status of a test. */ +export enum TestStatus { + TEST_STATUS_UNSPECIFIED = 'TEST_STATUS_UNSPECIFIED', + TEST_STATUS_NEW = 'TEST_STATUS_NEW', + TEST_STATUS_ASSIGNED = 'TEST_STATUS_ASSIGNED', + TEST_STATUS_RUNNING = 'TEST_STATUS_RUNNING', + TEST_STATUS_DONE = 'TEST_STATUS_DONE', + TEST_STATUS_SUSPENDED = 'TEST_STATUS_SUSPENDED', +} + +/** gRPC-friendly result of a test. */ +export enum TestResult { + TEST_RESULT_UNSPECIFIED = 'TEST_RESULT_UNSPECIFIED', + TEST_RESULT_PASS = 'TEST_RESULT_PASS', + TEST_RESULT_FAIL = 'TEST_RESULT_FAIL', + TEST_RESULT_ERROR = 'TEST_RESULT_ERROR', + TEST_RESULT_TIMEOUT = 'TEST_RESULT_TIMEOUT', + TEST_RESULT_ABORT = 'TEST_RESULT_ABORT', + TEST_RESULT_SKIP = 'TEST_RESULT_SKIP', +} + +/** gRPC-friendly status of a job. */ +export enum JobStatus { + JOB_STATUS_UNSPECIFIED = 'JOB_STATUS_UNSPECIFIED', + JOB_STATUS_NEW = 'JOB_STATUS_NEW', + JOB_STATUS_RUNNING = 'JOB_STATUS_RUNNING', + JOB_STATUS_DONE = 'JOB_STATUS_DONE', + JOB_STATUS_SUSPENDED = 'JOB_STATUS_SUSPENDED', + JOB_STATUS_ASSIGNED = 'JOB_STATUS_ASSIGNED', +} + +/** gRPC-friendly result of a job. */ +export enum JobResult { + JOB_RESULT_UNSPECIFIED = 'JOB_RESULT_UNSPECIFIED', + JOB_RESULT_PASS = 'JOB_RESULT_PASS', + JOB_RESULT_FAIL = 'JOB_RESULT_FAIL', + JOB_RESULT_ERROR = 'JOB_RESULT_ERROR', + JOB_RESULT_TIMEOUT = 'JOB_RESULT_TIMEOUT', + JOB_RESULT_ABORT = 'JOB_RESULT_ABORT', + JOB_RESULT_SKIP = 'JOB_RESULT_SKIP', +} + +/** gRPC-friendly status of a session. */ +export enum SessionStatus { + SESSION_STATUS_UNSPECIFIED = 'SESSION_STATUS_UNSPECIFIED', + SESSION_STATUS_NEW = 'SESSION_STATUS_NEW', + SESSION_STATUS_RUNNING = 'SESSION_STATUS_RUNNING', + SESSION_STATUS_DONE = 'SESSION_STATUS_DONE', +} + +/** gRPC-friendly result of a session. */ +export enum SessionResult { + SESSION_RESULT_UNSPECIFIED = 'SESSION_RESULT_UNSPECIFIED', + SESSION_RESULT_PASS = 'SESSION_RESULT_PASS', + SESSION_RESULT_FAIL = 'SESSION_RESULT_FAIL', + SESSION_RESULT_ERROR = 'SESSION_RESULT_ERROR', + SESSION_RESULT_TIMEOUT = 'SESSION_RESULT_TIMEOUT', + SESSION_RESULT_ABORT = 'SESSION_RESULT_ABORT', + SESSION_RESULT_SKIP = 'SESSION_RESULT_SKIP', +} + +/** Shared definition for troubleshooting error. */ +export declare interface ErrorInfo { + message: string; + trace?: string; +} + +/** Shared definition for troubleshooting warning. */ +export declare interface WarningInfo { + message: string; + trace?: string; +} + +/** Shared execution details mapped to the "Execution Details" UI cards. */ +export declare interface ExecutionDetails { + user?: string; + actualUser?: string; + cloudLogLink?: string; + createTime: string; + startTime?: string; + endTime?: string; + updateTime?: string; +} + +/** Collection container for error entry items. */ +export declare interface Errors { + error?: ErrorInfo[]; +} + +/** Collection container for warning entry items. */ +export declare interface Warnings { + warning?: WarningInfo[]; +} + +/** Troubleshooting layout card details shared by both Jobs and Tests. */ +export declare interface Troubleshooting { + warnings?: Warnings; + resultCause?: Errors; +} diff --git a/src/devtools/mobileharness/fe/v6/angular/app/core/models/test_overview.ts b/src/devtools/mobileharness/fe/v6/angular/app/core/models/test_overview.ts index 2b87e69092..756f5370c7 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/core/models/test_overview.ts +++ b/src/devtools/mobileharness/fe/v6/angular/app/core/models/test_overview.ts @@ -3,69 +3,28 @@ * presentation in the UI, corresponding to test details in mobileharness. */ +import { + ErrorInfo, + ExecutionDetails, + JobResult, + JobStatus, + SessionResult, + SessionStatus, + TestResult, + TestStatus, + Troubleshooting, + WarningInfo, +} from './common_models'; import {TimeBreakdown} from './timeline'; -export type {TimeBreakdown}; - -/** gRPC-friendly status of a test, decoupled from its result. */ -export enum TestStatus { - TEST_STATUS_UNSPECIFIED = 'TEST_STATUS_UNSPECIFIED', - TEST_STATUS_NEW = 'TEST_STATUS_NEW', - TEST_STATUS_ASSIGNED = 'TEST_STATUS_ASSIGNED', - TEST_STATUS_RUNNING = 'TEST_STATUS_RUNNING', - TEST_STATUS_DONE = 'TEST_STATUS_DONE', - TEST_STATUS_SUSPENDED = 'TEST_STATUS_SUSPENDED', -} - -/** gRPC-friendly result of a test. */ -export enum TestResult { - TEST_RESULT_UNSPECIFIED = 'TEST_RESULT_UNSPECIFIED', - TEST_RESULT_PASS = 'TEST_RESULT_PASS', - TEST_RESULT_FAIL = 'TEST_RESULT_FAIL', - TEST_RESULT_ERROR = 'TEST_RESULT_ERROR', - TEST_RESULT_TIMEOUT = 'TEST_RESULT_TIMEOUT', - TEST_RESULT_ABORT = 'TEST_RESULT_ABORT', - TEST_RESULT_SKIP = 'TEST_RESULT_SKIP', -} - -/** gRPC-friendly status of a job, decoupled from its result. */ -export enum JobStatus { - JOB_STATUS_UNSPECIFIED = 'JOB_STATUS_UNSPECIFIED', - JOB_STATUS_NEW = 'JOB_STATUS_NEW', - JOB_STATUS_RUNNING = 'JOB_STATUS_RUNNING', - JOB_STATUS_DONE = 'JOB_STATUS_DONE', - JOB_STATUS_SUSPENDED = 'JOB_STATUS_SUSPENDED', -} - -/** gRPC-friendly result of a job. */ -export enum JobResult { - JOB_RESULT_UNSPECIFIED = 'JOB_RESULT_UNSPECIFIED', - JOB_RESULT_PASS = 'JOB_RESULT_PASS', - JOB_RESULT_FAIL = 'JOB_RESULT_FAIL', - JOB_RESULT_ERROR = 'JOB_RESULT_ERROR', - JOB_RESULT_TIMEOUT = 'JOB_RESULT_TIMEOUT', - JOB_RESULT_ABORT = 'JOB_RESULT_ABORT', - JOB_RESULT_SKIP = 'JOB_RESULT_SKIP', -} - -/** gRPC-friendly status of a session, decoupled from its result. */ -export enum SessionStatus { - SESSION_STATUS_UNSPECIFIED = 'SESSION_STATUS_UNSPECIFIED', - SESSION_STATUS_NEW = 'SESSION_STATUS_NEW', - SESSION_STATUS_RUNNING = 'SESSION_STATUS_RUNNING', - SESSION_STATUS_DONE = 'SESSION_STATUS_DONE', -} - -/** gRPC-friendly result of a session. */ -export enum SessionResult { - SESSION_RESULT_UNSPECIFIED = 'SESSION_RESULT_UNSPECIFIED', - SESSION_RESULT_PASS = 'SESSION_RESULT_PASS', - SESSION_RESULT_FAIL = 'SESSION_RESULT_FAIL', - SESSION_RESULT_ERROR = 'SESSION_RESULT_ERROR', - SESSION_RESULT_TIMEOUT = 'SESSION_RESULT_TIMEOUT', - SESSION_RESULT_ABORT = 'SESSION_RESULT_ABORT', - SESSION_RESULT_SKIP = 'SESSION_RESULT_SKIP', -} - +export { + JobResult, + JobStatus, + SessionResult, + SessionStatus, + TestResult, + TestStatus, +}; +export type {ExecutionDetails, TimeBreakdown, Troubleshooting}; /** Minimal reference to the parent job. */ export interface ParentJobInfo { id: string; @@ -74,63 +33,24 @@ export interface ParentJobInfo { result?: JobResult; spongeLink?: string; } - /** One device the test was executed on. */ export interface TestDevice { id: string; type: string; } - /** Wrapper for a list of devices. */ export interface TestDevices { device?: TestDevice[]; } - /** The host the test ran on. */ export interface HostInfo { name: string; ip: string; } - -/** Execution details. */ -export interface ExecutionDetails { - user?: string; - actualUser?: string; - cloudLogLink?: string; - createTime: string; - startTime?: string; - endTime?: string; - lastUpdateTime?: string; -} - -/** One non-fatal warning surfaced during execution. */ -export interface TestWarning { - message: string; - trace?: string; -} - -/** Wrapper for a list of warnings. */ -export interface TestWarnings { - warning?: TestWarning[]; -} - -/** One error that contributed to a non-passing test result. */ -export interface TestError { - message: string; - trace?: string; -} - -/** Wrapper for a list of errors. */ -export interface TestErrors { - error?: TestError[]; -} - -/** Troubleshooting details (warnings & errors). */ -export interface Troubleshooting { - warnings?: TestWarnings; - resultCause?: TestErrors; -} - +/** Alias for a warning info item belonging to a test. */ +export type TestWarning = WarningInfo; +/** Alias for an error info item belonging to a test. */ +export type TestError = ErrorInfo; /** A lightweight summary of a test (for nested sub-tests). */ export interface TestSummary { id: string; @@ -141,18 +61,15 @@ export interface TestSummary { endTime?: string; devices?: TestDevices; } - /** Wrapper for a list of test summaries. */ export interface TestSummaries { test?: TestSummary[]; } - /** Nested sub-tests list details. */ export interface SubTestsInfo { rootTestId?: string; subTests?: TestSummaries; } - /** The full overview data for a single test (matches TestDetail proto). */ export interface TestOverviewData { id: string; @@ -168,25 +85,23 @@ export interface TestOverviewData { subTestsInfo?: SubTestsInfo; timingBreakdown?: TimeBreakdown; } - /** Request structure for the getTest API. */ export interface GetTestRequest { testId: string; subTestId?: string; + jobId: string; } - /** Request structure for the getTestLog API. */ export interface GetTestLogRequest { testId: string; offset: number; length: number; + jobId: string; } - /** Response structure for the getTest API. */ export interface GetTestResponse { test: TestOverviewData; } - /** Response structure for the getTestLog API. */ export interface GetTestLogResponse { logContent: string; diff --git a/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/tests/overview_error.ts b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/tests/overview_error.ts index 0a95d69ffb..d561160f9f 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/tests/overview_error.ts +++ b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/tests/overview_error.ts @@ -38,7 +38,7 @@ export const SCENARIO_TEST_ERROR: MockTestScenario = { createTime: '2025-07-09T10:15:00Z', startTime: '2025-07-09T10:15:05Z', endTime: '2025-07-09T10:15:10Z', - lastUpdateTime: '2025-07-09T10:15:10Z', + updateTime: '2025-07-09T10:15:10Z', user: 'dafeni', actualUser: 'dafeni@google.com', }, diff --git a/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/tests/overview_failed.ts b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/tests/overview_failed.ts index 662a49d4b6..4b989d2043 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/tests/overview_failed.ts +++ b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/tests/overview_failed.ts @@ -39,7 +39,7 @@ export const SCENARIO_TEST_FAILED: MockTestScenario = { createTime: '2025-07-09T10:12:10Z', startTime: '2025-07-09T10:12:10Z', endTime: '2025-07-09T10:12:40Z', - lastUpdateTime: '2025-07-09T10:12:40Z', + updateTime: '2025-07-09T10:12:40Z', user: 'dafeni', actualUser: 'dafeni@google.com', }, diff --git a/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/tests/overview_inprogress.ts b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/tests/overview_inprogress.ts index e51491bab0..10d89fde2d 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/tests/overview_inprogress.ts +++ b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/tests/overview_inprogress.ts @@ -45,7 +45,7 @@ export const SCENARIO_TEST_IN_PROGRESS: MockTestScenario = { createTime: '2025-07-09T11:30:10Z', startTime: '2025-07-09T11:30:15Z', endTime: '2025-07-09T11:30:18Z', - lastUpdateTime: '2025-07-09T11:30:18Z', + updateTime: '2025-07-09T11:30:18Z', user: 'dafeni', actualUser: 'dafeni@google.com', }, diff --git a/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/tests/overview_longname.ts b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/tests/overview_longname.ts index e528658faa..a1c56b7b1f 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/tests/overview_longname.ts +++ b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/tests/overview_longname.ts @@ -38,7 +38,7 @@ export const SCENARIO_TEST_LONG_NAME: MockTestScenario = { createTime: '2025-07-09T10:11:15Z', startTime: '2025-07-09T10:11:15Z', endTime: '2025-07-09T10:11:25Z', - lastUpdateTime: '2025-07-09T10:11:25Z', + updateTime: '2025-07-09T10:11:25Z', user: 'dafeni', actualUser: 'dafeni@google.com', }, diff --git a/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/tests/overview_multiple_errors_warnings.ts b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/tests/overview_multiple_errors_warnings.ts index 515e7330b7..d94c51b585 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/tests/overview_multiple_errors_warnings.ts +++ b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/tests/overview_multiple_errors_warnings.ts @@ -38,7 +38,7 @@ export const SCENARIO_TEST_MULTIPLE_ERRORS_WARNINGS: MockTestScenario = { createTime: '2025-07-09T10:15:00Z', startTime: '2025-07-09T10:15:05Z', endTime: '2025-07-09T10:15:20Z', - lastUpdateTime: '2025-07-09T10:15:20Z', + updateTime: '2025-07-09T10:15:20Z', user: 'dafeni', actualUser: 'dafeni@google.com', }, diff --git a/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/tests/overview_passed.ts b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/tests/overview_passed.ts index 8b3d87afbc..4f45006481 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/tests/overview_passed.ts +++ b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/tests/overview_passed.ts @@ -38,7 +38,7 @@ export const SCENARIO_TEST_PASSED: MockTestScenario = { createTime: '2025-07-09T10:11:15Z', startTime: '2025-07-09T10:11:15Z', endTime: '2025-07-09T10:11:25Z', - lastUpdateTime: '2025-07-09T10:11:25Z', + updateTime: '2025-07-09T10:11:25Z', user: 'dafeni', actualUser: 'dafeni@google.com', }, diff --git a/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/tests/overview_skipped.ts b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/tests/overview_skipped.ts index 61cfa99644..eaee6d9b78 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/tests/overview_skipped.ts +++ b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/tests/overview_skipped.ts @@ -37,7 +37,7 @@ export const SCENARIO_TEST_SKIPPED: MockTestScenario = { executionDetails: { createTime: '2025-07-09T10:11:15Z', endTime: '2025-07-09T10:11:16Z', - lastUpdateTime: '2025-07-09T10:11:16Z', + updateTime: '2025-07-09T10:11:16Z', user: 'dafeni', actualUser: 'dafeni@google.com', }, diff --git a/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/tests/overview_suspended.ts b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/tests/overview_suspended.ts index efd093b6cf..71dac1eddf 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/tests/overview_suspended.ts +++ b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/tests/overview_suspended.ts @@ -36,7 +36,7 @@ export const SCENARIO_TEST_SUSPENDED: MockTestScenario = { }, executionDetails: { createTime: '2025-07-09T11:40:00Z', - lastUpdateTime: '2025-07-09T11:45:00Z', + updateTime: '2025-07-09T11:45:00Z', user: 'blaze-user', actualUser: 'mobileharness-ci-runner', }, diff --git a/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/tests/overview_timeout.ts b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/tests/overview_timeout.ts index 8291189906..d66ab1d9c8 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/tests/overview_timeout.ts +++ b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/tests/overview_timeout.ts @@ -38,7 +38,7 @@ export const SCENARIO_TEST_TIMEOUT: MockTestScenario = { createTime: '2025-07-09T10:20:00Z', startTime: '2025-07-09T10:20:05Z', endTime: '2025-07-09T10:25:05Z', - lastUpdateTime: '2025-07-09T10:25:05Z', + updateTime: '2025-07-09T10:25:05Z', user: 'dafeni', actualUser: 'dafeni@google.com', }, diff --git a/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/tests/overview_warning.ts b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/tests/overview_warning.ts index 2069b16c38..ce8ea7072e 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/tests/overview_warning.ts +++ b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/tests/overview_warning.ts @@ -38,7 +38,7 @@ export const SCENARIO_TEST_WARNING: MockTestScenario = { createTime: '2025-07-09T10:11:15Z', startTime: '2025-07-09T10:11:15Z', endTime: '2025-07-09T10:11:25Z', - lastUpdateTime: '2025-07-09T10:11:25Z', + updateTime: '2025-07-09T10:11:25Z', user: 'dafeni', actualUser: 'dafeni@google.com', }, diff --git a/src/devtools/mobileharness/fe/v6/angular/app/core/services/test/fake_test_service.ts b/src/devtools/mobileharness/fe/v6/angular/app/core/services/test/fake_test_service.ts index 8b77da7ccd..a50abec986 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/core/services/test/fake_test_service.ts +++ b/src/devtools/mobileharness/fe/v6/angular/app/core/services/test/fake_test_service.ts @@ -19,7 +19,10 @@ import {TestService} from './test_service'; providedIn: 'root', }) export class FakeTestService extends TestService { - private readonly logLimits = new Map(); + // Tracks test IDs that have finished streaming all their simulated running logs. + private readonly completedRunningTests = new Set(); + // Tracks the number of generated lines for running tests. + private readonly runningLineOffsets = new Map(); constructor() { super(); @@ -27,6 +30,9 @@ export class FakeTestService extends TestService { /** * Retrieves the detailed overview data for a specific test by its ID from the mock dataset. + * + * @param request The request containing the test ID or sub-test ID. + * @return An Observable emitting the TestOverviewData. */ override getTest(request: GetTestRequest): Observable { const id = request.subTestId || request.testId; @@ -34,9 +40,23 @@ export class FakeTestService extends TestService { (s) => s.id === id || s.overview.id === id, ); if (scenario) { + let status = scenario.overview.status; + // In production, running tests eventually transition to DONE. + // Once all logs of a running test have been streamed, simulate test completion. + if ( + status === TestStatus.TEST_STATUS_RUNNING && + this.completedRunningTests.has(scenario.id) + ) { + status = TestStatus.TEST_STATUS_DONE; + } + console.log( + `[FakeTestService] getTest id=${scenario.id} status=${status} completedHas=${this.completedRunningTests.has(scenario.id)}`, + ); + const execDetails = scenario.overview.executionDetails; const overview: TestOverviewData = { ...scenario.overview, + status, executionDetails: execDetails ? { ...execDetails, @@ -53,14 +73,13 @@ export class FakeTestService extends TestService { } /** - * Mock implementation of getTestLog that streams log chunks from the test scenario. + * Mock implementation of getTestLog that streams log chunks progressively from the test scenario. */ override getTestLog( request: GetTestLogRequest, ): Observable { const id = request.testId; - const offset = request.offset; - const length = request.length; + const numOffset = Number(request.offset) || 0; const scenario = MOCK_TEST_SCENARIOS.find( (s) => s.id === id || s.overview.id === id, ); @@ -68,40 +87,60 @@ export class FakeTestService extends TestService { const fullLog = scenario.log || ''; const status = scenario.overview.status; - let visibleLog = ''; + // When starting fresh from offset 0, reset completion status to simulate a new run. + if (numOffset === 0) { + this.completedRunningTests.delete(scenario.id); + this.runningLineOffsets.delete(scenario.id); + } + if (status === TestStatus.TEST_STATUS_RUNNING) { - // Stream line-by-line if test is active const lines = fullLog.split('\n'); - if (Number(offset) === 0) { - this.logLimits.set(id, Math.min(2, lines.length)); - } else { - const currentLineLimit = this.logLimits.get(id) || 0; - const newLineLimit = Math.min(currentLineLimit + 2, lines.length); - this.logLimits.set(id, newLineLimit); + const currentLineOffset = this.runningLineOffsets.get(scenario.id) || 0; + // Progressively add 3 lines on each poll to simulate production logs growing. + const newLineOffset = Math.min(currentLineOffset + 3, lines.length); + this.runningLineOffsets.set(scenario.id, newLineOffset); + + const visibleLog = + lines.slice(0, newLineOffset).join('\n') + + (newLineOffset < lines.length ? '\n' : ''); + const chunk = visibleLog.substring(numOffset); + const nextOffset = numOffset + chunk.length; + + console.log( + `[FakeTestService] getTestLog id=${scenario.id} offset=${numOffset} chunkLen=${chunk.length} nextOffset=${nextOffset} fullLogLen=${fullLog.length} linesEmitted=${newLineOffset}/${lines.length}`, + ); + + if (newLineOffset >= lines.length) { + this.completedRunningTests.add(scenario.id); } - const visibleLineLimit = this.logLimits.get(id) || lines.length; - visibleLog = lines.slice(0, visibleLineLimit).join('\n'); + + const hasMore = false; + + return of({ + logContent: chunk, + nextOffset, + hasMore, + }).pipe(delay(300)); } else { - // If test is DONE/terminated, return the entire log in one call (adaptive return) - visibleLog = fullLog; - } + // If test is DONE/terminated, return remaining log content. + const chunk = fullLog.substring(numOffset); + const nextOffset = numOffset + chunk.length; + const hasMore = false; - const chunk = visibleLog.substring( - Number(offset), - Number(offset) + length, - ); - const nextOffset = Number(offset) + chunk.length; - const hasMore = nextOffset < visibleLog.length; + console.log( + `[FakeTestService] getTestLog DONE id=${scenario.id} offset=${numOffset} chunkLen=${chunk.length} nextOffset=${nextOffset} fullLogLen=${fullLog.length}`, + ); - return of({ - logContent: chunk, - nextOffset, - hasMore, - }).pipe(delay(500)); + return of({ + logContent: chunk, + nextOffset, + hasMore, + }).pipe(delay(300)); + } } else { return throwError( () => new Error(`Test with ID '${id}' not found in mock data.`), - ).pipe(delay(500)); + ).pipe(delay(300)); } } } diff --git a/src/devtools/mobileharness/fe/v6/angular/app/core/services/test/http_test_service.ts b/src/devtools/mobileharness/fe/v6/angular/app/core/services/test/http_test_service.ts index aff5d95438..bbd3860dae 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/core/services/test/http_test_service.ts +++ b/src/devtools/mobileharness/fe/v6/angular/app/core/services/test/http_test_service.ts @@ -25,12 +25,12 @@ export class HttpTestService extends TestService { } override getTest(request: GetTestRequest): Observable { - let url = `${this.apiUrl}/${request.testId}`; - if (request.subTestId) { - url += `?sub_test_id=${request.subTestId}`; - } return this.http - .get(url) + .post(`${this.apiUrl}/${request.testId}:getTest`, { + 'test_id': request.testId, + 'sub_test_id': request.subTestId, + 'job_id': request.jobId, + }) .pipe(map((response) => response.test)); } @@ -40,8 +40,10 @@ export class HttpTestService extends TestService { return this.http.post( `${this.apiUrl}/${request.testId}:getTestLog`, { + 'test_id': request.testId, 'offset': request.offset, 'length': request.length, + 'job_id': request.jobId, }, ); } diff --git a/src/devtools/mobileharness/fe/v6/angular/app/core/utils/url_utils.ts b/src/devtools/mobileharness/fe/v6/angular/app/core/utils/url_utils.ts index 12afd49217..6273626907 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/core/utils/url_utils.ts +++ b/src/devtools/mobileharness/fe/v6/angular/app/core/utils/url_utils.ts @@ -16,10 +16,24 @@ export function navigateWithPreservedParams( const newUrl = new URL(url, window.location.origin); // Get current query parameters from the active route. const currentParams = route.snapshot.queryParamMap; + // CLIENT-SIDE ROUTER TRANSITION SANITIZATION: + // Remove 'universe' when navigating to test or job details pages. + // This prevents propagating key parameters like 'universe=google_1p' from monitoring pages + // (like host/device details where universe is valid) to test/job details pages where they're invalid. + // Note: This acts on internal application nav link clicks, while stripUniverseGuard sanitizes + // external entrypoints, and universeInterceptor manages the HTTP request layer. + const isJobOrTest = /\/(jobs|tests)\//.test(newUrl.pathname); + if (isJobOrTest) { + newUrl.searchParams.delete('universe'); + } + // Merge the current query parameters into the new URL if they are not already present. // Note: The new URL parameters have high priority and will NOT be overridden by current parameters. // This ensures parameters like `is_embedded_mode` are preserved across navigations if not specified in new URL. for (const key of currentParams.keys) { + if (key === 'universe' && isJobOrTest) { + continue; + } if (!newUrl.searchParams.has(key)) { const values = currentParams.getAll(key); for (const value of values) { diff --git a/src/devtools/mobileharness/fe/v6/angular/app/features/dev_harness/dev_harness_page.ng.html b/src/devtools/mobileharness/fe/v6/angular/app/features/dev_harness/dev_harness_page.ng.html index 74d58726cc..cfdb59d61c 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/features/dev_harness/dev_harness_page.ng.html +++ b/src/devtools/mobileharness/fe/v6/angular/app/features/dev_harness/dev_harness_page.ng.html @@ -38,7 +38,7 @@ } @else {
- @for (dev of test.devices?.device; track dev.id) { + @for (dev of devices; track dev.id) { {{ dev.id }} }
} - @if (test.host && test.host.name && test.host.ip) { + @if (host && host.name && host.ip) {
Host
{{ test.host.name }}{{ host.name }} -
{{ test.host.ip }}
+
{{ host.ip }}
} @@ -205,7 +208,12 @@

{{ test.name }}

@if (activeTab() === 'log') {
@defer { - + } @placeholder {
Loading logs...
} diff --git a/src/devtools/mobileharness/fe/v6/angular/app/features/test_detail/test_detail.spec.ts b/src/devtools/mobileharness/fe/v6/angular/app/features/test_detail/test_detail.spec.ts index c3bee76f59..9c9259b32a 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/features/test_detail/test_detail.spec.ts +++ b/src/devtools/mobileharness/fe/v6/angular/app/features/test_detail/test_detail.spec.ts @@ -118,7 +118,10 @@ describe('TestDetail Component', () => { }); it('should fetch test details on init', () => { - expect(mockTestService.getTest).toHaveBeenCalledWith({testId: 'test_123'}); + expect(mockTestService.getTest).toHaveBeenCalledWith({ + testId: 'test_123', + jobId: '', + }); const pageData = component.testPageData(); expect(pageData).toBeTruthy(); expect(pageData!.testOverviewData!.id).toBe('test_123'); diff --git a/src/devtools/mobileharness/fe/v6/angular/app/features/test_detail/test_detail.ts b/src/devtools/mobileharness/fe/v6/angular/app/features/test_detail/test_detail.ts index d8b2585268..4905995273 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/features/test_detail/test_detail.ts +++ b/src/devtools/mobileharness/fe/v6/angular/app/features/test_detail/test_detail.ts @@ -86,23 +86,28 @@ export class TestDetail { queryParams.get('sub_test_id') || queryParams.get('sub_test') || undefined, + jobId: params.get('jobId') || '', })), distinctUntilChanged( - (a, b) => a.testId === b.testId && a.subTestId === b.subTestId, + (a, b) => + a.testId === b.testId && + a.subTestId === b.subTestId && + a.jobId === b.jobId, ), tap(() => { this.loadingService.show(); }), - switchMap(({testId, subTestId}) => { + switchMap(({testId, subTestId, jobId}) => { if (!testId) { this.loadingService.hide(); return of({ testOverviewData: null, error: 'No test ID provided in the route.', + jobId, }); } - const request: GetTestRequest = {testId}; + const request: GetTestRequest = {testId, jobId}; if (subTestId) { request.subTestId = subTestId; } @@ -110,7 +115,9 @@ export class TestDetail { const idForErrorLogging = subTestId || testId; return this.testService.getTest(request).pipe( - map((testOverviewData) => ({testOverviewData}) as TestPageData), + map( + (testOverviewData) => ({testOverviewData, jobId}) as TestPageData, + ), tap((data) => { if (data?.testOverviewData) { const test = data.testOverviewData; @@ -126,6 +133,7 @@ export class TestDetail { return of({ testOverviewData: null, error: `Failed to load test data for ID: ${idForErrorLogging}. ${err.message || ''}`, + jobId, }); }), finalize(() => { diff --git a/src/devtools/mobileharness/fe/v6/angular/app/shared/components/accordion_item/accordion_item.scss b/src/devtools/mobileharness/fe/v6/angular/app/shared/components/accordion_item/accordion_item.scss index 31b799913c..92ee87bf07 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/shared/components/accordion_item/accordion_item.scss +++ b/src/devtools/mobileharness/fe/v6/angular/app/shared/components/accordion_item/accordion_item.scss @@ -24,6 +24,7 @@ min-width: 0; padding-right: 16px; word-break: break-word; + font-family: config.$labconsole-font; } .arrow-icon { diff --git a/src/devtools/mobileharness/fe/v6/service/proto/test/BUILD b/src/devtools/mobileharness/fe/v6/service/proto/test/BUILD index 5d1d81fc1d..2a3088c542 100644 --- a/src/devtools/mobileharness/fe/v6/service/proto/test/BUILD +++ b/src/devtools/mobileharness/fe/v6/service/proto/test/BUILD @@ -44,6 +44,7 @@ proto_library( srcs = ["test_service.proto"], deps = [ ":test_resources_proto", + "@googleapis//google/api:annotations_proto", ], ) diff --git a/src/devtools/mobileharness/fe/v6/service/proto/test/test_service.proto b/src/devtools/mobileharness/fe/v6/service/proto/test/test_service.proto index 7035b92657..e3ff9db9c2 100644 --- a/src/devtools/mobileharness/fe/v6/service/proto/test/test_service.proto +++ b/src/devtools/mobileharness/fe/v6/service/proto/test/test_service.proto @@ -18,6 +18,7 @@ syntax = "proto3"; package devtools.mobileharness.fe.v6.service.proto.test; +import "google/api/annotations.proto"; import "src/devtools/mobileharness/fe/v6/service/proto/test/test_resources.proto"; option java_package = "com.google.devtools.mobileharness.fe.v6.service.proto.test"; @@ -26,10 +27,20 @@ option java_outer_classname = "TestServiceProto"; service TestService { // Gets the detailed information of a test by its ID. - rpc GetTest(GetTestRequest) returns (GetTestResponse) {} + rpc GetTest(GetTestRequest) returns (GetTestResponse) { + option (google.api.http) = { + post: "/v6/tests/{test_id}:getTest" + body: "*" + }; + } // Gets the log chunk of a test by its ID, supporting pagination/offset. - rpc GetTestLog(GetTestLogRequest) returns (GetTestLogResponse) {} + rpc GetTestLog(GetTestLogRequest) returns (GetTestLogResponse) { + option (google.api.http) = { + post: "/v6/tests/{test_id}:getTestLog" + body: "*" + }; + } } message GetTestRequest { diff --git a/src/java/com/google/devtools/mobileharness/fe/v6/server/BUILD b/src/java/com/google/devtools/mobileharness/fe/v6/server/BUILD index 523b50c765..57575f0ccf 100644 --- a/src/java/com/google/devtools/mobileharness/fe/v6/server/BUILD +++ b/src/java/com/google/devtools/mobileharness/fe/v6/server/BUILD @@ -44,6 +44,7 @@ java_library( "//src/java/com/google/devtools/mobileharness/fe/v6/service/host:host_service_grpc_impl", "//src/java/com/google/devtools/mobileharness/fe/v6/service/host:host_service_module", "//src/java/com/google/devtools/mobileharness/fe/v6/service/shared:oss_stubs", + "//src/java/com/google/devtools/mobileharness/fe/v6/service/test", "//src/java/com/google/devtools/mobileharness/fe/v6/shared/util/concurrent:oss_executor_module", "//src/java/com/google/devtools/mobileharness/shared/util/flags", "//src/java/com/google/devtools/mobileharness/shared/util/flags/core:flags_manager", diff --git a/src/java/com/google/devtools/mobileharness/fe/v6/server/OssFeServer.java b/src/java/com/google/devtools/mobileharness/fe/v6/server/OssFeServer.java index df3c86761f..0b144496da 100644 --- a/src/java/com/google/devtools/mobileharness/fe/v6/server/OssFeServer.java +++ b/src/java/com/google/devtools/mobileharness/fe/v6/server/OssFeServer.java @@ -28,6 +28,8 @@ import com.google.devtools.mobileharness.fe.v6.service.host.HostServiceGrpcImpl; import com.google.devtools.mobileharness.fe.v6.service.host.HostServiceModule; import com.google.devtools.mobileharness.fe.v6.service.shared.OssStubsModule; +import com.google.devtools.mobileharness.fe.v6.service.test.OssTestServiceModule; +import com.google.devtools.mobileharness.fe.v6.service.test.TestServiceGrpcImpl; import com.google.devtools.mobileharness.fe.v6.shared.util.concurrent.OssExecutorModule; import com.google.devtools.mobileharness.shared.util.flags.Flags; import com.google.devtools.mobileharness.shared.util.flags.core.FlagsManager; @@ -50,6 +52,7 @@ public final class OssFeServer { private final HostServiceGrpcImpl hostService; private final ConfigServiceGrpcImpl configService; private final AdminServiceGrpcImpl adminService; + private final TestServiceGrpcImpl testService; private volatile Server grpcServer; @Inject @@ -58,11 +61,13 @@ public final class OssFeServer { HostServiceGrpcImpl hostService, ConfigServiceGrpcImpl configService, AdminServiceGrpcImpl adminService, + TestServiceGrpcImpl testService, @ServerPort int port) { this.deviceService = deviceService; this.hostService = hostService; this.configService = configService; this.adminService = adminService; + this.testService = testService; this.port = port; } @@ -74,6 +79,7 @@ public void start() throws IOException { .addService(hostService) .addService(configService) .addService(adminService) + .addService(testService) .addService(ProtoReflectionService.newInstance()) .build(); grpcServer.start(); @@ -107,6 +113,7 @@ public static void main(String[] args) throws IOException, InterruptedException new HostServiceModule(), new ConfigServiceModule(), new AdminServiceModule(), + new OssTestServiceModule(), new OssStubsModule(), new AbstractModule() { @Override diff --git a/src/java/com/google/devtools/mobileharness/fe/v6/service/test/BUILD b/src/java/com/google/devtools/mobileharness/fe/v6/service/test/BUILD new file mode 100644 index 0000000000..dcec50f75e --- /dev/null +++ b/src/java/com/google/devtools/mobileharness/fe/v6/service/test/BUILD @@ -0,0 +1,35 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +load("@rules_java//java:java_library.bzl", "java_library") + +package( + default_applicable_licenses = ["//:license"], + default_visibility = ["//src/devtools/mobileharness/fe/v6:visibility"], +) + +java_library( + name = "test", + srcs = glob(["*.java"]), + deps = [ + "//src/devtools/mobileharness/fe/v6/service/proto/test:test_service_java_grpc", + "//src/devtools/mobileharness/fe/v6/service/proto/test:test_service_java_proto", + "//src/java/com/google/devtools/common/metrics/stability/rpc/grpc:service_util", + "@grpc-java//stub", + "@maven//:com_google_guava_guava", + "@maven//:com_google_inject_guice", + "@maven//:javax_inject_jsr330_api", + ], +) diff --git a/src/java/com/google/devtools/mobileharness/fe/v6/service/test/NoOpTestServiceLogic.java b/src/java/com/google/devtools/mobileharness/fe/v6/service/test/NoOpTestServiceLogic.java new file mode 100644 index 0000000000..f3badbff13 --- /dev/null +++ b/src/java/com/google/devtools/mobileharness/fe/v6/service/test/NoOpTestServiceLogic.java @@ -0,0 +1,41 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.devtools.mobileharness.fe.v6.service.test; + +import static com.google.common.util.concurrent.Futures.immediateFailedFuture; + +import com.google.common.util.concurrent.ListenableFuture; +import com.google.devtools.mobileharness.fe.v6.service.proto.test.GetTestLogRequest; +import com.google.devtools.mobileharness.fe.v6.service.proto.test.GetTestLogResponse; +import com.google.devtools.mobileharness.fe.v6.service.proto.test.GetTestRequest; +import com.google.devtools.mobileharness.fe.v6.service.proto.test.GetTestResponse; + +/** No-op implementation of {@link TestServiceLogic} used when no MOSS backend is available. */ +public final class NoOpTestServiceLogic implements TestServiceLogic { + + @Override + public ListenableFuture getTest(GetTestRequest request) { + return immediateFailedFuture( + new UnsupportedOperationException("TestService.GetTest is not available.")); + } + + @Override + public ListenableFuture getTestLog(GetTestLogRequest request) { + return immediateFailedFuture( + new UnsupportedOperationException("TestService.GetTestLog is not available.")); + } +} diff --git a/src/java/com/google/devtools/mobileharness/fe/v6/service/test/OssTestServiceModule.java b/src/java/com/google/devtools/mobileharness/fe/v6/service/test/OssTestServiceModule.java new file mode 100644 index 0000000000..27a76152de --- /dev/null +++ b/src/java/com/google/devtools/mobileharness/fe/v6/service/test/OssTestServiceModule.java @@ -0,0 +1,27 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.devtools.mobileharness.fe.v6.service.test; + +import com.google.inject.AbstractModule; + +/** OSS Guice module for TestService. Binds {@link TestServiceLogic} to the no-op impl. */ +public final class OssTestServiceModule extends AbstractModule { + @Override + protected void configure() { + bind(TestServiceLogic.class).to(NoOpTestServiceLogic.class); + } +} diff --git a/src/java/com/google/devtools/mobileharness/fe/v6/service/test/TestServiceGrpcImpl.java b/src/java/com/google/devtools/mobileharness/fe/v6/service/test/TestServiceGrpcImpl.java new file mode 100644 index 0000000000..c439e0a224 --- /dev/null +++ b/src/java/com/google/devtools/mobileharness/fe/v6/service/test/TestServiceGrpcImpl.java @@ -0,0 +1,63 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.devtools.mobileharness.fe.v6.service.test; + +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.devtools.common.metrics.stability.rpc.grpc.GrpcServiceUtil; +import com.google.devtools.mobileharness.fe.v6.service.proto.test.GetTestLogRequest; +import com.google.devtools.mobileharness.fe.v6.service.proto.test.GetTestLogResponse; +import com.google.devtools.mobileharness.fe.v6.service.proto.test.GetTestRequest; +import com.google.devtools.mobileharness.fe.v6.service.proto.test.GetTestResponse; +import com.google.devtools.mobileharness.fe.v6.service.proto.test.TestServiceGrpc; +import io.grpc.stub.StreamObserver; +import javax.inject.Inject; + +/** gRPC implementation of the TestService. */ +public final class TestServiceGrpcImpl extends TestServiceGrpc.TestServiceImplBase { + + private final TestServiceLogic logic; + private final ListeningExecutorService executor; + + @Inject + TestServiceGrpcImpl(TestServiceLogic logic, ListeningExecutorService executor) { + this.logic = logic; + this.executor = executor; + } + + @Override + public void getTest(GetTestRequest request, StreamObserver responseObserver) { + GrpcServiceUtil.invokeAsync( + request, + responseObserver, + logic::getTest, + executor, + TestServiceGrpc.getServiceDescriptor(), + TestServiceGrpc.getGetTestMethod()); + } + + @Override + public void getTestLog( + GetTestLogRequest request, StreamObserver responseObserver) { + GrpcServiceUtil.invokeAsync( + request, + responseObserver, + logic::getTestLog, + executor, + TestServiceGrpc.getServiceDescriptor(), + TestServiceGrpc.getGetTestLogMethod()); + } +} diff --git a/src/java/com/google/devtools/mobileharness/fe/v6/service/test/TestServiceLogic.java b/src/java/com/google/devtools/mobileharness/fe/v6/service/test/TestServiceLogic.java new file mode 100644 index 0000000000..d70b0a636a --- /dev/null +++ b/src/java/com/google/devtools/mobileharness/fe/v6/service/test/TestServiceLogic.java @@ -0,0 +1,33 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.devtools.mobileharness.fe.v6.service.test; + +import com.google.common.util.concurrent.ListenableFuture; +import com.google.devtools.mobileharness.fe.v6.service.proto.test.GetTestLogRequest; +import com.google.devtools.mobileharness.fe.v6.service.proto.test.GetTestLogResponse; +import com.google.devtools.mobileharness.fe.v6.service.proto.test.GetTestRequest; +import com.google.devtools.mobileharness.fe.v6.service.proto.test.GetTestResponse; + +/** Logic interface for the Test Detail service. */ +public interface TestServiceLogic { + + /** Gets the full test detail (overview, execution details, troubleshooting, sub-tests). */ + ListenableFuture getTest(GetTestRequest request); + + /** Gets a paginated chunk of the test log. */ + ListenableFuture getTestLog(GetTestLogRequest request); +} diff --git a/src/javatests/com/google/devtools/mobileharness/fe/v6/server/BUILD b/src/javatests/com/google/devtools/mobileharness/fe/v6/server/BUILD index 831671d7c7..b4fa304431 100644 --- a/src/javatests/com/google/devtools/mobileharness/fe/v6/server/BUILD +++ b/src/javatests/com/google/devtools/mobileharness/fe/v6/server/BUILD @@ -32,6 +32,7 @@ java_library( "//src/java/com/google/devtools/mobileharness/fe/v6/service/device:device_service_module", "//src/java/com/google/devtools/mobileharness/fe/v6/service/host:host_service_module", "//src/java/com/google/devtools/mobileharness/fe/v6/service/shared:oss_stubs", + "//src/java/com/google/devtools/mobileharness/fe/v6/service/test", "//src/java/com/google/devtools/mobileharness/fe/v6/shared/util/concurrent:oss_executor_module", "@maven//:com_google_inject_guice", "@maven//:junit_junit", diff --git a/src/javatests/com/google/devtools/mobileharness/fe/v6/server/OssFeServerTest.java b/src/javatests/com/google/devtools/mobileharness/fe/v6/server/OssFeServerTest.java index 5619b5911d..4f2968fbd6 100644 --- a/src/javatests/com/google/devtools/mobileharness/fe/v6/server/OssFeServerTest.java +++ b/src/javatests/com/google/devtools/mobileharness/fe/v6/server/OssFeServerTest.java @@ -22,6 +22,7 @@ import com.google.devtools.mobileharness.fe.v6.service.device.DeviceServiceModule; import com.google.devtools.mobileharness.fe.v6.service.host.HostServiceModule; import com.google.devtools.mobileharness.fe.v6.service.shared.OssStubsModule; +import com.google.devtools.mobileharness.fe.v6.service.test.OssTestServiceModule; import com.google.devtools.mobileharness.fe.v6.shared.util.concurrent.OssExecutorModule; import com.google.inject.AbstractModule; import com.google.inject.Guice; @@ -43,6 +44,7 @@ public void startAndStop() throws Exception { new HostServiceModule(), new ConfigServiceModule(), new AdminServiceModule(), + new OssTestServiceModule(), new OssStubsModule(), new AbstractModule() { @Override