diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 07179431a..868599527 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -59,6 +59,49 @@ jobs: - name: Build C++ examples working-directory: photonlib-cpp-examples run: ./gradlew build + playwright-tests: + name: "Playwright E2E tests" + runs-on: ubuntu-22.04 + needs: [validation] + steps: + # Checkout code. + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Fetch tags + run: git fetch --tags --force + - name: Install Java 17 + uses: actions/setup-java@v4 + with: + java-version: 17 + distribution: temurin + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Install mrcal deps + run: sudo apt-get update && sudo apt-get install -y libcholmod3 liblapack3 libsuitesparseconfig5 + - name: Setup tests + working-directory: photon-client + run: | + pnpm install + pnpm test-setup + - name: Prebuild Gradle + run: ./gradlew photon-targeting:build photon-core:build photon-server:build -x check + - name: Run Playwright tests + working-directory: photon-client + run: pnpm test + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: photon-client/playwright-report/ + retention-days: 30 build-gradle: name: "Gradle Build" runs-on: ubuntu-22.04 diff --git a/.gitignore b/.gitignore index 6416c2fba..6a3ddc0b6 100644 --- a/.gitignore +++ b/.gitignore @@ -146,5 +146,13 @@ networktables.json photon-server/src/main/resources/web/* node_modules dist +dist-ssr components.d.ts photon-server/src/main/resources/web/index.html + +# Playwright +photon-client/test-results/ +photon-client/playwright-report/ +photon-client/blob-report/ +photon-client/playwright/.cache/ +photon-client/playwright/.auth/ diff --git a/photon-client/.gitignore b/photon-client/.gitignore deleted file mode 100644 index 0f57d3477..000000000 --- a/photon-client/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -node_modules -.DS_Store -dist -dist-ssr - -# Editor directories and files -.idea -components.d.ts diff --git a/photon-client/eslint.config.mjs b/photon-client/eslint.config.mjs index 792d7dcc6..435241641 100644 --- a/photon-client/eslint.config.mjs +++ b/photon-client/eslint.config.mjs @@ -8,7 +8,7 @@ export default defineConfigWithVueTs( vueTsConfigs.recommended, skipFormattingConfig, { - ignores: ["**/dist/**"] + ignores: ["**/dist/**", "playwright-report"] }, { //extends: ["js/recommended"], diff --git a/photon-client/package.json b/photon-client/package.json index cf2485b2d..db4a7230f 100644 --- a/photon-client/package.json +++ b/photon-client/package.json @@ -9,9 +9,13 @@ "build": "vite build", "build-demo": "vite build --mode demo", "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix", - "format": "prettier --write src/", + "format": "prettier --write src/ tests/", "lint-ci": "eslint . --max-warnings 0 --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts", - "format-ci": "prettier --check src/" + "format-ci": "prettier --check src/", + "test": "playwright test", + "test-ui": "playwright test --ui", + "test-setup": "playwright install --with-deps" + }, "dependencies": { "@fontsource/prompt": "^5.2.6", @@ -28,6 +32,7 @@ }, "devDependencies": { "@eslint/js": "^9.31.0", + "@playwright/test": "^1.56.1", "@types/node": "^22.15.14", "@types/three": "^0.178.0", "@vitejs/plugin-vue": "^6.0.0", diff --git a/photon-client/playwright.config.ts b/photon-client/playwright.config.ts new file mode 100644 index 000000000..8ce62cbda --- /dev/null +++ b/photon-client/playwright.config.ts @@ -0,0 +1,83 @@ +import { defineConfig, devices } from "@playwright/test"; +import path from "path"; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + globalSetup: "./tests/global-setup", + testDir: "./tests", + /* Run tests in files in parallel */ + fullyParallel: false, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: 1, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: "html", + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('')`. */ + baseURL: "http://localhost:5800", + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry" + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] } + }, + + { + name: "firefox", + use: { ...devices["Desktop Firefox"] } + }, + + { + name: "webkit", + use: { ...devices["Desktop Safari"] } + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + { + name: "Microsoft Edge", + use: { ...devices["Desktop Edge"], channel: "msedge" } + } + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: process.platform == "win32" ? "" : "./" + "gradlew run", + url: "http://localhost:5800", + timeout: 300 * 1000, + reuseExistingServer: !process.env.CI, + cwd: path.normalize("../") + } +}); diff --git a/photon-client/pnpm-lock.yaml b/photon-client/pnpm-lock.yaml index b93de8144..380943670 100644 --- a/photon-client/pnpm-lock.yaml +++ b/photon-client/pnpm-lock.yaml @@ -45,6 +45,9 @@ importers: '@eslint/js': specifier: ^9.31.0 version: 9.31.0 + '@playwright/test': + specifier: ^1.56.1 + version: 1.56.1 '@types/node': specifier: ^22.15.14 version: 22.15.14 @@ -430,6 +433,11 @@ packages: resolution: {integrity: sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@playwright/test@1.56.1': + resolution: {integrity: sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==} + engines: {node: '>=18'} + hasBin: true + '@rolldown/pluginutils@1.0.0-beta.19': resolution: {integrity: sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==} @@ -1019,6 +1027,11 @@ packages: resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} engines: {node: '>= 6'} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1247,6 +1260,16 @@ packages: typescript: optional: true + playwright-core@1.56.1: + resolution: {integrity: sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.56.1: + resolution: {integrity: sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==} + engines: {node: '>=18'} + hasBin: true + postcss-selector-parser@6.1.2: resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} engines: {node: '>=4'} @@ -1759,6 +1782,10 @@ snapshots: '@pkgr/core@0.2.4': {} + '@playwright/test@1.56.1': + dependencies: + playwright: 1.56.1 + '@rolldown/pluginutils@1.0.0-beta.19': {} '@rollup/rollup-android-arm-eabi@4.40.2': @@ -2399,6 +2426,9 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -2605,6 +2635,14 @@ snapshots: optionalDependencies: typescript: 5.8.3 + playwright-core@1.56.1: {} + + playwright@1.56.1: + dependencies: + playwright-core: 1.56.1 + optionalDependencies: + fsevents: 2.3.2 + postcss-selector-parser@6.1.2: dependencies: cssesc: 3.0.0 diff --git a/photon-client/src/components/settings/ObjectDetectionCard.vue b/photon-client/src/components/settings/ObjectDetectionCard.vue index 53435b3d8..7b9a2b987 100644 --- a/photon-client/src/components/settings/ObjectDetectionCard.vue +++ b/photon-client/src/components/settings/ObjectDetectionCard.vue @@ -224,6 +224,7 @@ const handleBulkImport = () => { v-model="importVersion" variant="underlined" label="Model Version" + data-testid="import-version-select" :items=" useSettingsStore().general.supportedBackends?.includes('RKNN') ? ['YOLOv5', 'YOLOv8', 'YOLO11'] @@ -324,7 +325,7 @@ const handleBulkImport = () => { Info - + {{ model.nickname }} {{ model.labels.join(", ") }} diff --git a/photon-client/tests/fixtures.ts b/photon-client/tests/fixtures.ts new file mode 100644 index 000000000..b5bedca41 --- /dev/null +++ b/photon-client/tests/fixtures.ts @@ -0,0 +1,16 @@ +import { test as base } from "@playwright/test"; +import axios from "axios"; + +export const test = base.extend({ + page: async ({ page }, use) => { + // Use the page in the test (no per-test backend reset here) + axios.defaults.baseURL = "http://localhost:5800/api/test"; + await use(page); + } +}); + +test.beforeAll(async () => { + console.log("Running before all tests: Resetting backend state..."); + await axios.post("http://localhost:5800/api/test/resetBackend"); + await axios.post("http://localhost:5800/api/test/activateTestMode"); +}); diff --git a/photon-client/tests/global-setup.ts b/photon-client/tests/global-setup.ts new file mode 100644 index 000000000..cfb7a590e --- /dev/null +++ b/photon-client/tests/global-setup.ts @@ -0,0 +1,7 @@ +async function globalSetup() { + // You can perform global setup tasks here, such as starting a server or setting environment variables + const path = await import("path"); + process.env.TESTS_DIR = path.resolve(process.cwd()); +} + +export default globalSetup; diff --git a/photon-client/tests/platformDependent/od.spec.ts b/photon-client/tests/platformDependent/od.spec.ts new file mode 100644 index 000000000..2ac32a194 --- /dev/null +++ b/photon-client/tests/platformDependent/od.spec.ts @@ -0,0 +1,79 @@ +import { expect } from "@playwright/test"; +import { test } from "../fixtures"; +import axios from "axios"; +import path from "path"; + +const fakeModelName = "FAKE-MODEL"; +const fakeLabels = "test, 1, woof"; +const newModelName = "foo-bar"; +const platforms = ["LINUX_RK3588_64", "LINUX_QCS6490"]; + +for (const platform of platforms) { + test.describe(`Platform: ${platform}`, () => { + test.beforeEach(async ({ page }) => { + await page.goto("/#/settings"); + await axios.post("/override/platform", { platform: platform }); + await page.reload(); + }); + + test("testSettingsPage", async ({ page }) => { + if (platform.endsWith("RK3588_64")) { + await expect(page.getByRole("main")).toContainText("Linux AARCH 64-bit with RK3588"); + } else if (platform.endsWith("QCS6490")) { + await expect(page.getByRole("main")).toContainText("Linux AARCH 64-bit with QCS6490"); + } + await expect(page.getByText("Object Detection")).toBeVisible(); + }); + + test("Upload model", async ({ page }) => { + const testsDir = process.env.TESTS_DIR; + if (!testsDir) { + throw new Error("TESTS_DIR is not set"); + } + + await page.getByRole("button", { name: "Import Model" }).click(); + await page.getByRole("textbox", { name: "Labels" }).fill(fakeLabels); + await page.getByRole("spinbutton", { name: "Width" }).fill("640"); + await page.getByRole("spinbutton", { name: "Height" }).fill("640"); + await page.getByTestId("import-version-select").click(); + await page.getByRole("option", { name: "YOLOv8" }).click(); + + const modelFile = platform.endsWith("RK3588_64") ? `${fakeModelName}.rknn` : `${fakeModelName}.tflite`; + await page + .getByRole("button", { name: "Model File Model File" }) + .setInputFiles(path.join(testsDir, "tests/resources", modelFile)); + + await page.getByRole("button", { name: "Import Object Detection Model" }).click(); + + await page.goto("/#/settings"); + const tableRow = page.getByTestId("model-table").locator("tr", { hasText: fakeModelName }); + + await expect(tableRow).toBeVisible(); + await expect(tableRow).toContainText(fakeLabels); + }); + + test("Rename model", async ({ page }) => { + const tableRow = page.getByTestId("model-table").locator("tr", { hasText: fakeModelName }); + + await tableRow.getByRole("button", { name: "Rename Model" }).click(); + await page.getByRole("textbox", { name: "New Name New Name" }).fill(newModelName); + await page.getByRole("button", { name: "Rename", exact: true }).click(); + + await page.reload(); + + const renamedRow = page.getByTestId("model-table").locator("tr", { hasText: newModelName }); + await expect(renamedRow).toContainText(fakeLabels); + }); + + test("Delete model", async ({ page }) => { + const tableRow = page.getByTestId("model-table").locator("tr", { hasText: newModelName }); + + await tableRow.getByRole("button", { name: "Delete Model" }).click(); + await page.getByRole("button", { name: "Delete model", exact: true }).click(); + + await page.reload(); + const deletedRow = page.getByTestId("model-table").locator("tr", { hasText: newModelName }); + await expect(deletedRow).toHaveCount(0); + }); + }); +} diff --git a/photon-client/tests/resources/FAKE-MODEL.rknn b/photon-client/tests/resources/FAKE-MODEL.rknn new file mode 100644 index 000000000..e69de29bb diff --git a/photon-client/tests/resources/FAKE-MODEL.tflite b/photon-client/tests/resources/FAKE-MODEL.tflite new file mode 100644 index 000000000..e69de29bb diff --git a/photon-core/src/main/java/org/photonvision/common/configuration/NeuralNetworkModelManager.java b/photon-core/src/main/java/org/photonvision/common/configuration/NeuralNetworkModelManager.java index d1063ded4..6b789402e 100644 --- a/photon-core/src/main/java/org/photonvision/common/configuration/NeuralNetworkModelManager.java +++ b/photon-core/src/main/java/org/photonvision/common/configuration/NeuralNetworkModelManager.java @@ -212,12 +212,23 @@ public class NeuralNetworkModelManager { } /** - * Returns the singleton instance of the NeuralNetworkModelManager + * Returns the singleton instance of the NeuralNetworkModelManager. Call getInstance() to use the + * default (no reset), or getInstance(true) to reset. * * @return The singleton instance */ public static NeuralNetworkModelManager getInstance() { - if (INSTANCE == null) { + return getInstance(false); + } + + /** + * Returns the singleton instance of the NeuralNetworkModelManager, optionally resetting it. + * + * @param reset If true, resets the instance + * @return The singleton instance + */ + public static NeuralNetworkModelManager getInstance(boolean reset) { + if (INSTANCE == null || reset) { INSTANCE = new NeuralNetworkModelManager(); } return INSTANCE; diff --git a/photon-server/src/main/java/org/photonvision/server/RequestHandler.java b/photon-server/src/main/java/org/photonvision/server/RequestHandler.java index 67b60070a..b655cb656 100644 --- a/photon-server/src/main/java/org/photonvision/server/RequestHandler.java +++ b/photon-server/src/main/java/org/photonvision/server/RequestHandler.java @@ -74,6 +74,12 @@ public class RequestHandler { private static final ObjectMapper kObjectMapper = new ObjectMapper(); + private static boolean testMode = false; + + public static void setTestMode(boolean isTestMode) { + testMode = isTestMode; + } + private record CommonCameraUniqueName(String cameraUniqueName) {} public static void onSettingsImportRequest(Context ctx) { @@ -662,35 +668,36 @@ public class RequestHandler { ModelProperties modelProperties = new ModelProperties(modelPath, nickname, labels, width, height, family, version); - ObjectDetector objDetector = null; - - try { - objDetector = - switch (family) { - case RUBIK -> new RubikModel(modelProperties).load(); - case RKNN -> new RknnModel(modelProperties).load(); - }; - } catch (RuntimeException e) { - ctx.status(400); - ctx.result("Failed to load object detection model: " + e.getMessage()); + if (!testMode) { + ObjectDetector objDetector = null; try { - Files.deleteIfExists(modelPath); - } catch (IOException ex) { - e.addSuppressed(ex); - } + objDetector = + switch (family) { + case RUBIK -> new RubikModel(modelProperties).load(); + case RKNN -> new RknnModel(modelProperties).load(); + }; + } catch (RuntimeException e) { + ctx.status(400); + ctx.result("Failed to load object detection model: " + e.getMessage()); - logger.error("Failed to load object detection model", e); - return; - } finally { - // this finally block will run regardless of what happens in try/catch - // please see https://docs.oracle.com/javase/tutorial/essential/exceptions/finally.html - // for a summary on how finally works - if (objDetector != null) { - objDetector.release(); + try { + Files.deleteIfExists(modelPath); + } catch (IOException ex) { + e.addSuppressed(ex); + } + + logger.error("Failed to load object detection model", e); + return; + } finally { + // this finally block will run regardless of what happens in try/catch + // please see https://docs.oracle.com/javase/tutorial/essential/exceptions/finally.html + // for a summary on how finally works + if (objDetector != null) { + objDetector.release(); + } } } - ConfigManager.getInstance() .getConfig() .neuralNetworkPropertyManager() diff --git a/photon-server/src/main/java/org/photonvision/server/Server.java b/photon-server/src/main/java/org/photonvision/server/Server.java index ec54caf25..ee2922c26 100644 --- a/photon-server/src/main/java/org/photonvision/server/Server.java +++ b/photon-server/src/main/java/org/photonvision/server/Server.java @@ -162,6 +162,13 @@ public class Server { app.post("/api/objectdetection/rename", RequestHandler::onRenameObjectDetectionModelRequest); app.post("/api/objectdetection/nuke", RequestHandler::onNukeObjectDetectionModelsRequest); + /* Testing API Events */ + + app.post("/api/test/resetBackend", TestRequestHandler::handleResetRequest); + + app.post("/api/test/activateTestMode", TestRequestHandler::testMode); + app.post("/api/test/override/platform", TestRequestHandler::handlePlatformOverrideRequest); + app.start(port); } diff --git a/photon-server/src/main/java/org/photonvision/server/TestRequestHandler.java b/photon-server/src/main/java/org/photonvision/server/TestRequestHandler.java new file mode 100644 index 000000000..6f888c78a --- /dev/null +++ b/photon-server/src/main/java/org/photonvision/server/TestRequestHandler.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.photonvision.server; + +import io.javalin.http.Context; +import org.photonvision.common.configuration.ConfigManager; +import org.photonvision.common.configuration.NeuralNetworkModelManager; +import org.photonvision.common.hardware.Platform; +import org.photonvision.common.logging.LogGroup; +import org.photonvision.common.logging.Logger; +import org.photonvision.common.util.file.JacksonUtils; + +public class TestRequestHandler { + // Treat all 2XX calls as "INFO" + // Treat all 4XX calls as "ERROR" + // Treat all 5XX calls as "ERROR" + + static Logger logger = new Logger(TestRequestHandler.class, LogGroup.WebServer); + + public static void handleResetRequest(Context ctx) { + logger.info("Resetting Backend"); + // Reset backend + ConfigManager.nukeConfigDirectory(); + ConfigManager.getInstance().load(); + } + + private record PlatformOverrideRequest(Platform platform) {} + + public static void handlePlatformOverrideRequest(Context ctx) { + try { + PlatformOverrideRequest request = + JacksonUtils.deserialize(ctx.body(), PlatformOverrideRequest.class); + Platform platform = request.platform(); + logger.info("Overriding platform to: " + platform); + + Platform.overridePlatform(platform); + NeuralNetworkModelManager.getInstance(true).extractModels(); + NeuralNetworkModelManager.getInstance().discoverModels(); + ctx.status(200); + + } catch (Exception e) { + logger.error("Failed to parse platform override request: " + e.getMessage()); + ctx.status(400).result("Invalid request"); + } + } + + public static void testMode(Context ctx) { + logger.info("Test mode activated"); + RequestHandler.setTestMode(true); + ctx.status(200).result("Test mode activated"); + } +} diff --git a/photon-targeting/src/main/java/org/photonvision/common/hardware/Platform.java b/photon-targeting/src/main/java/org/photonvision/common/hardware/Platform.java index efe06760e..1080f40ab 100644 --- a/photon-targeting/src/main/java/org/photonvision/common/hardware/Platform.java +++ b/photon-targeting/src/main/java/org/photonvision/common/hardware/Platform.java @@ -102,7 +102,8 @@ public enum Platform { public final boolean isSupported; // Set once at init, shouldn't be needed after. - private static final Platform currentPlatform = getCurrentPlatform(); + private static Platform currentPlatform = getCurrentPlatform(); + private static boolean override = false; Platform( String description, @@ -119,6 +120,11 @@ public enum Platform { this.nativeLibraryFolderName = nativeLibFolderName; } + public static void overridePlatform(Platform platform) { + currentPlatform = platform; + override = true; + } + ////////////////////////////////////////////////////// // Public API @@ -128,14 +134,15 @@ public enum Platform { } public static boolean isRK3588() { - return Platform.isOrangePi() + return currentPlatform == LINUX_RK3588_64 + || Platform.isOrangePi() || Platform.isCoolPi4b() || Platform.isRock5C() || fileHasText("/proc/device-tree/compatible", "rk3588"); } public static boolean isQCS6490() { - return isRubik(); + return currentPlatform == LINUX_QCS6490 || Platform.isRubik(); } public static boolean isRaspberryPi() { @@ -167,6 +174,11 @@ public enum Platform { return runRobotFile.exists(); } + public static boolean isWindows() { + var p = getCurrentPlatform(); + return (p == WINDOWS_32 || p == WINDOWS_64); + } + ////////////////////////////////////////////////////// // Debug info related to unknown platforms for debug help @@ -177,6 +189,10 @@ public enum Platform { private static final String UnknownDeviceModelString = "Unknown"; public static Platform getCurrentPlatform() { + if (override) { + return currentPlatform; + } + String OS_NAME; if (Platform.OS_NAME != null) { OS_NAME = Platform.OS_NAME; @@ -319,9 +335,4 @@ public enum Platform { return false; } } - - public static boolean isWindows() { - var p = getCurrentPlatform(); - return (p == WINDOWS_32 || p == WINDOWS_64); - } }