diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index db4cbde755d..4c44095c64d 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -11,10 +11,8 @@ on: profile: required: false default: "release" - type: string + type: choice options: ["release", "debug", "production"] - push: - branches: ["try-android"] env: RUST_BACKTRACE: 1 diff --git a/.github/workflows/dispatch-workflow.yml b/.github/workflows/dispatch-workflow.yml new file mode 100644 index 00000000000..0ff3087f905 --- /dev/null +++ b/.github/workflows/dispatch-workflow.yml @@ -0,0 +1,55 @@ +name: Dispatch Workflow +on: + workflow_call: + inputs: + workflow: + required: true + type: string + profile: + required: true + type: string + wpt-tests-to-run: + required: true + type: string + wpt-layout: + required: true + type: string + unit-tests: + required: true + type: boolean + +jobs: + win: + if: ${{ inputs.workflow == 'windows' }} + uses: ./.github/workflows/windows.yml + secrets: inherit + with: + profile: ${{ inputs.profile }} + unit-tests: ${{ inputs.unit-tests }} + + macos: + if: ${{ inputs.workflow == 'macos' }} + uses: ./.github/workflows/mac.yml + secrets: inherit + with: + profile: ${{ inputs.profile }} + wpt-layout: ${{ inputs.wpt-layout }} + unit-tests: ${{ inputs.unit-tests }} + wpt-tests-to-run: ${{ inputs.wpt-tests-to-run }} + + linux: + if: ${{ inputs.workflow == 'linux' }} + uses: ./.github/workflows/linux.yml + secrets: inherit + with: + profile: ${{ inputs.profile }} + wpt-layout: ${{ inputs.wpt-layout }} + unit-tests: ${{ inputs.unit-tests }} + wpt-tests-to-run: ${{ inputs.wpt-tests-to-run }} + + android: + if: ${{ inputs.workflow == 'android' }} + uses: ./.github/workflows/android.yml + secrets: inherit + with: + profile: ${{ inputs.profile }} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 600be396d4d..4edc30f16cf 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,124 +11,36 @@ on: branches: ["**"] merge_group: types: [checks_requested] - workflow_call: - inputs: - configuration: - required: true - type: string workflow_dispatch: - inputs: - profile: - required: false - default: "release" - type: choice - options: ["release", "debug", "production"] - wpt-tests-to-run: - default: "" - required: false - type: string - platform: - required: false - type: choice - options: ["linux", "windows", "macos", "all"] - wpt-layout: - required: false - type: choice - options: ["none", "2013", "2020", "all"] - unit-tests: - required: false - type: boolean jobs: - decision: - name: Decision - runs-on: ubuntu-20.04 - outputs: - configuration: ${{ steps.configuration.outputs.result }} - steps: - - name: Configuration - id: configuration - uses: actions/github-script@v6 - with: - script: | - // If this is a workflow call with a configuration object, - // then just return it immediately. - let configuration = ${{ inputs.configuration || '""' }}; - if (configuration != "") { - console.log("Using configuration: " + JSON.stringify(configuration)); - return configuration; - } - - // We need to pick defaults if the inputs are not provided. Unprovided inputs - // are empty strings in this template. - let platform = "${{ inputs.platform }}" || "linux"; - let profile = "${{ inputs.profile }}" || "release"; - let wpt_layout = "${{ inputs.wpt-layout }}" || "none"; - let wpt_tests_to_run = "${{ inputs.wpt-tests-to-run }}" || ""; - let unit_tests = Boolean(${{ inputs.unit-tests }}) - - // Merge queue runs and pushes to `main` should always trigger a full build and test. - if (["push", "merge_group"].includes(context.eventName)) { - platform = "all"; - wpt_layout = "all"; - unit_tests = true; - } - - let platforms = []; - if (platform == "all") { - platforms = [ "linux", "windows", "macos", "android" ]; - } else { - platforms = [ platform ]; - } - - let returnValue = { - platforms, - wpt_layout, - unit_tests, - profile, - wpt_tests_to_run, - }; - - console.log("Using configuration: " + JSON.stringify(returnValue)); - return returnValue; - build-win: name: Windows - needs: ["decision"] - if: ${{ contains(fromJson(needs.decision.outputs.configuration).platforms, 'windows') }} + if: ${{ github.event_name != 'pull_request' }} uses: ./.github/workflows/windows.yml with: - profile: ${{ fromJson(needs.decision.outputs.configuration).profile }} - unit-tests: ${{ fromJson(needs.decision.outputs.configuration).unit_tests }} + unit-tests: true secrets: inherit build-mac: name: Mac - needs: ["decision"] - if: ${{ contains(fromJson(needs.decision.outputs.configuration).platforms, 'macos') }} + if: ${{ github.event_name != 'pull_request' }} uses: ./.github/workflows/mac.yml with: - wpt-tests-to-run: ${{ fromJson(needs.decision.outputs.configuration).wpt_tests_to_run }} - profile: ${{ fromJson(needs.decision.outputs.configuration).profile }} - unit-tests: ${{ fromJson(needs.decision.outputs.configuration).unit_tests }} + unit-tests: true secrets: inherit build-linux: name: Linux - needs: ["decision"] - if: ${{ contains(fromJson(needs.decision.outputs.configuration).platforms, 'linux') }} uses: ./.github/workflows/linux.yml with: - wpt-tests-to-run: ${{ fromJson(needs.decision.outputs.configuration).wpt_tests_to_run }} - profile: ${{ fromJson(needs.decision.outputs.configuration).profile }} - wpt-layout: ${{ fromJson(needs.decision.outputs.configuration).wpt_layout }} - unit-tests: ${{ fromJson(needs.decision.outputs.configuration).unit_tests }} + unit-tests: true + wpt-layout: ${{ github.event_name == 'pull_request' && 'none' || 'all' }} secrets: inherit build-android: name: Android - needs: ["decision"] - if: ${{ contains(fromJson(needs.decision.outputs.configuration).platforms, 'android') }} + if: ${{ github.event_name != 'pull_request' }} uses: ./.github/workflows/android.yml with: profile: "release" @@ -140,15 +52,11 @@ jobs: if: always() # needs all build to detect cancellation needs: - - "decision" - "build-win" - "build-mac" - "build-linux" - "build-android" steps: - - name: Mark skipped jobs as successful - if: ${{ fromJson(needs.decision.outputs.configuration).platforms[0] != null }} - run: exit 0 - name: Mark the job as successful if: ${{ !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }} run: exit 0 diff --git a/.github/workflows/try.yml b/.github/workflows/try.yml index 301b74deadd..d9886f72d0e 100644 --- a/.github/workflows/try.yml +++ b/.github/workflows/try.yml @@ -1,203 +1,123 @@ name: Try on: - pull_request_target: - types: [labeled] push: - branches: ["try", "try-linux", "try-mac", "try-wpt-mac", "try-wpt-mac-2020", "try-wpt", "try-wpt-2020", "try-windows"] + branches: ["try"] + workflow_dispatch: + inputs: + profile: + required: false + default: "release" + type: choice + options: ["release", "debug", "production"] + wpt-tests-to-run: + default: "" + required: false + type: string + wpt-layout: + required: false + type: choice + options: ["none", "2013", "2020", "all"] + unit-tests: + required: false + type: boolean jobs: - parse-comment: - name: Trigger Try - runs-on: ubuntu-latest - concurrency: - group: try-${{ github.event.number }} + decision: + name: Generate Try Configuration + runs-on: ubuntu-20.04 outputs: configuration: ${{ steps.configuration.outputs.result }} steps: - - uses: actions/github-script@v6 + - uses: actions/setup-python@v5 + with: + python-version: '3.10' + - uses: actions/checkout@v3 + with: + fetch-depth: 1 + sparse-checkout: | + python/servo/try_parser.py + sparse-checkout-cone-mode: false + - name: Get Full Configuration + id: full_config + run: | + { + echo 'config<> $GITHUB_OUTPUT + - name: Configuration id: configuration + uses: actions/github-script@v6 with: script: | - function makeComment(body) { - console.log(body); + // When triggered via a push try the `try` branch, search the last commit for a configuration object. + if (${{ github.ref_name == 'try' }}) { + let commit_msg = context.payload.head_commit.message; + try { + var config = JSON.parse(commit_msg.split('\n').slice(-1)); - if (context.eventName != 'pull_request_target') - return; - - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body - }) - } - - function combineWPTLayoutOptions(layout, newLayout) { - let has2013 = layout == "2013" || layout == "all"; - let has2020 = layout == "2020" || layout == "all"; - let adding2013 = newLayout == "2013"; - let adding2020 = newLayout == "2020"; - - if ((adding2020 && has2020) || (adding2013 && has2013)) { - return layout; + if (config && typeof config === "object") { + console.log("Using try commit configuration: " + JSON.stringify(config)); + return config; } - if (adding2020) { - return has2013 ? "all" : "2020"; - } - if (adding2013) { - return has2020 ? "all" : "2013"; - } - return layout; - } - - function addPlatformToConfiguration(platform, configuration) { - if (!configuration.platforms.includes(platform)) { - configuration.platforms.push(platform); + } + catch (exception) { + console.log("Could not parse try configuration from commit message: " + exception); + console.log("Triggering full try run."); } } - function updateConfigurationFromString(tryString, configuration) { - if (tryString.includes("full")) { - configuration.platforms = ["linux", "macos", "windows"]; - configuration.unit_tests = true; - configuration.wpt_layout = "all"; - return configuration; - } + // If we reach here we are likely doing a full run. + configuration = ${{ steps.full_config.outputs.config }}; - if (tryString.includes("linux")) { - addPlatformToConfiguration("linux", configuration); - configuration.unit_tests = true; - } else if (tryString.includes("mac")) { - addPlatformToConfiguration("macos", configuration); - configuration.unit_tests = true; - } else if (tryString.includes("win")) { - addPlatformToConfiguration("windows", configuration); - configuration.unit_tests = true; - } + // Process `workflow_dispatch` provided configuration overrides. + if (context.eventName == "workflow_dispatch") { + // WPT-related overrides only affect Linux currently, as tests don't run by default on other platforms. + configuration.matrix[0].wpt_layout = "${{ inputs.wpt-layout }}" || "none"; + configuration.matrix[0].wpt_tests_to_run = "${{ inputs.wpt-tests-to-run }}" || ""; - if (tryString.includes("wpt")) { - addPlatformToConfiguration("linux", configuration); - if (tryString.includes("2020")) { - configuration.wpt_layout = combineWPTLayoutOptions(configuration.wpt_layout, "2020"); - } else { - configuration.wpt_layout = combineWPTLayoutOptions(configuration.wpt_layout, "2013"); - } - } - } - - let configuration = { - platforms: [], - wpt_layout: "none", - unit_tests: false, - profile: "release", - wpt_tests_to_run: "", - }; - - if (context.eventName == 'pull_request_target') { - for (const label of context.payload.pull_request.labels) { - if (!label.name.startsWith("T-")) { - continue; - } - - // Try to remove the label. If that fails, it's likely that another - // workflow has already processed it or a user has removed it. - try { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - name: label.name, - }); - } catch (exception) { - console.log("Assuming '" + label.name + "' is already removed: " + exception); - continue; - } - - console.log("Found label: " + label.name); - updateConfigurationFromString(label.name, configuration); - } - } else { - let ref_name = "${{ github.ref_name || 'empty' }}"; - if (ref_name == "try") { - updateConfigurationFromString("full", configuration); - } else { - updateConfigurationFromString(ref_name, configuration); + let unit_tests = Boolean(${{ inputs.unit-tests }}); + let profile = '${{ inputs.profile }}'; + for (const platform of configuration.matrix) { + platform.profile = profile; + platform.unit_tests = unit_tests; } } - console.log(JSON.stringify(configuration)); - - if (configuration.platforms.length == 0) { - return { platforms: [] }; - } - - let username = context.payload.sender.login; - let result = await github.rest.repos.getCollaboratorPermissionLevel({ - owner: context.repo.owner, - repo: context.repo.repo, - username - }); - if (!result.data.user.permissions.push) { - makeComment('🔒 User @' + username + ' does not have permission to trigger try jobs.'); - return { platforms: [] }; - } - - const url = context.serverUrl + - "/" + context.repo.owner + - "/" + context.repo.repo + - "/actions/runs/" + context.runId; - const formattedURL = "[#" + context.runId + "](" + url + ")"; - let platformsString = configuration.platforms.toString(); - makeComment("🔨 Triggering try run (" + formattedURL + ") with platforms=" + - platformsString + " and layout=" + configuration.wpt_layout); + console.log("Using configuration: " + JSON.stringify(configuration)); return configuration; - run-try: - name: Run Try - needs: ["parse-comment"] - if: ${{ fromJson(needs.parse-comment.outputs.configuration).platforms[0] != null }} - uses: ./.github/workflows/main.yml + build: + needs: ["decision"] + name: ${{ matrix.name }} + strategy: + fail-fast: ${{ fromJson(needs.decision.outputs.configuration).fail_fast }} + matrix: + include: ${{ fromJson(needs.decision.outputs.configuration).matrix }} + # We need to use `dipatch-workflow.yml` because workflows do not support using: ${} + uses: ./.github/workflows/dispatch-workflow.yml + secrets: inherit with: - configuration: ${{ needs.parse-comment.outputs.configuration }} + workflow: ${{ matrix.workflow }} + wpt-layout: ${{ matrix.wpt_layout }} + profile: ${{ matrix.profile }} + unit-tests: ${{ matrix.unit_tests }} + wpt-tests-to-run: ${{ matrix.wpt_tests_to_run }} - results: - name: Results - needs: ["parse-comment", "run-try"] + build-result: + name: Result runs-on: ubuntu-latest - if: ${{ always() && fromJson(needs.parse-comment.outputs.configuration).platforms[0] != null }} + if: always() + # `needs: "build"` is necessary to detect cancellation. + needs: + - "decision" + - "build" + steps: - - name: Success - if: ${{ github.event_name == 'pull_request_target' && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }} - uses: actions/github-script@v6 - with: - script: | - const url = context.serverUrl + - "/" + context.repo.owner + - "/" + context.repo.repo + - "/actions/runs/" + context.runId; - const formattedURL = "[#" + context.runId + "](" + url + ")"; - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: "✨ Try run (" + formattedURL + ") " + "succeeded.", - }); - - name: Failure - if: ${{ github.event_name == 'pull_request_target' && contains(needs.*.result, 'failure') }} - uses: actions/github-script@v6 - with: - script: | - const url = context.serverUrl + - "/" + context.repo.owner + - "/" + context.repo.repo + - "/actions/runs/" + context.runId; - const formattedURL = "[#" + context.runId + "](" + url + ")"; - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: "⚠️ Try run (" + formattedURL + ") " + "failed.", - }); - - + - name: Mark the job as successful + if: ${{ !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }} + run: exit 0 + - name: Mark the job as unsuccessful + if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') + run: exit 1 diff --git a/.github/workflows/try_labels.yml b/.github/workflows/try_labels.yml new file mode 100644 index 00000000000..1b7859f62d4 --- /dev/null +++ b/.github/workflows/try_labels.yml @@ -0,0 +1,184 @@ +name: Try (Label) + +on: + pull_request_target: + types: [labeled] + +jobs: + parse-comment: + name: Trigger Try + runs-on: ubuntu-latest + concurrency: + group: try-${{ github.event.number }} + outputs: + configuration: ${{ steps.configuration.outputs.config }} + try_string: ${{ steps.try_string.outputs.result }} + steps: + - name: Collect Labels + uses: actions/github-script@v6 + id: try_string + with: + result-encoding: string + script: | + function makeComment(body) { + console.log(body); + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body + }) + } + + let try_string = ""; + + for (let label of context.payload.pull_request.labels) { + if (!label.name.startsWith("T-")) { + continue; + } + + // Try to remove the label. If that fails, it's likely that another + // workflow has already processed it or a user has removed it. + try { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + name: label.name, + }); + } catch (exception) { + console.log("Assuming '" + label.name + "' is already removed: " + exception); + continue; + } + + console.log("Found label: " + label.name); + // Remove the "T-" prefix. + label = label.name.slice(2); + try_string += " " + label; + } + + console.log(try_string); + + // Exit early if the try string is empty (no try triggered). + if (!try_string.trim()) { + return ""; + } + + let username = context.payload.sender.login; + let result = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username + }); + if (!result.data.user.permissions.push) { + makeComment('🔒 User @' + username + ' does not have permission to trigger try jobs.'); + return ""; + } + + return try_string; + - uses: actions/setup-python@v5 + with: + python-version: '3.10' + - uses: actions/checkout@v3 + with: + # This is necessary to checkout the pull request if this run was triggered + # via an `label` action on a pull request. + ref: refs/pull/${{ github.event.number }}/head + fetch-depth: 1 + sparse-checkout: | + python/servo/try_parser.py + sparse-checkout-cone-mode: false + - name: Parse Labels + if: ${{ steps.try_string.outputs.result }} + id: configuration + run: | + { + echo 'config<> $GITHUB_OUTPUT + - name: Comment Run Start + if: ${{ steps.try_string.outputs.result }} + uses: actions/github-script@v6 + with: + result-encoding: string + script: | + let config = ${{ steps.configuration.outputs.config }}; + function makeComment(body) { + console.log(body); + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body + }) + } + + const url = context.serverUrl + + "/" + context.repo.owner + + "/" + context.repo.repo + + "/actions/runs/" + context.runId; + const formattedURL = "[#" + context.runId + "](" + url + ")"; + makeComment("🔨 Triggering try run (" + formattedURL + ") for " + + config.matrix.map(m => m.name).join(", ")); + + run-try: + if: ${{ needs.parse-comment.outputs.try_string }} + needs: ["parse-comment"] + name: ${{ matrix.name }} + strategy: + fail-fast: ${{ fromJson(needs.parse-comment.outputs.configuration).fail_fast }} + matrix: + include: ${{ fromJson(needs.parse-comment.outputs.configuration).matrix }} + # We need to use `dipatch-workflow.yml` because workflows do not support using: ${}. + uses: ./.github/workflows/dispatch-workflow.yml + secrets: inherit + with: + workflow: ${{ matrix.workflow }} + wpt-layout: ${{ matrix.wpt_layout }} + profile: ${{ matrix.profile }} + unit-tests: ${{ matrix.unit_tests }} + wpt-tests-to-run: ${{ matrix.wpt_tests_to_run }} + + results: + name: Results + needs: ["parse-comment", "run-try"] + runs-on: ubuntu-latest + if: ${{ always() && needs.parse-comment.outputs.try_string }} + steps: + - name: Success + if: ${{ !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }} + uses: actions/github-script@v6 + with: + script: | + const url = context.serverUrl + + "/" + context.repo.owner + + "/" + context.repo.repo + + "/actions/runs/" + context.runId; + const formattedURL = "[#" + context.runId + "](" + url + ")"; + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: "✨ Try run (" + formattedURL + ") " + "succeeded.", + }); + - name: Failure + if: ${{ contains(needs.*.result, 'failure') }} + uses: actions/github-script@v6 + with: + script: | + const url = context.serverUrl + + "/" + context.repo.owner + + "/" + context.repo.repo + + "/actions/runs/" + context.runId; + const formattedURL = "[#" + context.runId + "](" + url + ")"; + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: "⚠️ Try run (" + formattedURL + ") " + "failed.", + }); + + diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index ae3eb29ddc1..f0ce9e08a23 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -4,7 +4,8 @@ on: workflow_call: inputs: profile: - required: true + required: false + default: "release" type: string unit-tests: required: false diff --git a/python/servo/devenv_commands.py b/python/servo/devenv_commands.py index dd4009fe8c2..ed260cf8b3b 100644 --- a/python/servo/devenv_commands.py +++ b/python/servo/devenv_commands.py @@ -22,17 +22,7 @@ from mach.decorators import ( ) from servo.command_base import CommandBase, cd, call - -VALID_TRY_BRACHES = [ - "try", - "try-linux", - "try-mac", - "try-windows", - "try-wpt", - "try-wpt-2020", - "try-wpt-mac", - "try-wpt-mac-2020" -] +from servo.try_parser import Config @CommandProvider @@ -284,38 +274,35 @@ class MachCommands(CommandBase): return p.wait() @Command('try', - description='Runs try jobs by force pushing to personal fork try branches', + description='Runs try jobs by force pushing to try branch', category='devenv') @CommandArgument( - 'jobs', default=["try"], nargs='...', - help="Name(s) of job(s) (ex: try, linux, mac, windows, wpt)") - def try_jobs(self, jobs): - branches = [] - # we validate branches because force pushing is destructive - for job in jobs: - # branches must start with try- - if "try" not in job: - job = "try-" + job - if job not in VALID_TRY_BRACHES: - print(job + " job doesn't exist") - return -1 - branches.append(job) - remote = "origin" - if "servo/servo" in subprocess.check_output(["git", "config", "--get", "remote.origin.url"]).decode(): - # if we have servo/servo for origin check try remote - try: - if "servo/servo" in subprocess.check_output(["git", "config", "--get", "remote.try.url"]).decode(): - # User has servo/servo for try remote - print("You should not use servo/servo for try remote!") - return -1 - else: - remote = "try" - except subprocess.CalledProcessError: - print("It looks like you are patching in upstream servo.") - print("Set try remote to your personal fork with `git remote add try https://github.com/user/servo`") - return -1 - for b in branches: - res = call(["git", "push", remote, "--force", f"HEAD:{b}"], env=self.build_env()) - if res != 0: - return res - return 0 + '--remote', '-r', default="origin", + help='git remote to use for try pushes') + @CommandArgument( + 'try_string', default=None, nargs='...', + help="Try string") + def try_jobs(self, remote="origin", try_string=None): + if not try_string: + try_string = "full" + else: + try_string = " ".join(try_string) + conf = Config(try_string) + result = call(["git", "commit", "--allow-empty", "-m", try_string, "-m", f"{conf.to_json()}"]) + if result != 0: + return result + + git_remote = subprocess.check_output(["git", "config", "--get", f"remote.{remote}.url"]).decode() + if "servo/servo" in git_remote: + print("WARNING: You are triggering try build in upstream repo!") + + result = call(["git", "push", remote, "--force", "HEAD:try"]) + if result != 0: + return result + + git_remote = git_remote.replace(".git", "/actions") + print(f"You can find triggered workflow here: {git_remote}") + + # Remove the last commit which only contains the try configuration. + result += call(["git", "reset", "--soft", "HEAD~1"]) + return result diff --git a/python/servo/testing_commands.py b/python/servo/testing_commands.py index 2a2080b9f42..f93cb75ff0c 100644 --- a/python/servo/testing_commands.py +++ b/python/servo/testing_commands.py @@ -244,6 +244,10 @@ class MachCommands(CommandBase): print("Running tidy tests...") passed = tidy.run_tests() and passed + import python.servo.try_parser as try_parser + print("Running try_parser tests...") + passed = try_parser.run_tests() and passed + print("Running WPT tests...") passed = wpt.run_tests() and passed diff --git a/python/servo/try_parser.py b/python/servo/try_parser.py new file mode 100644 index 00000000000..6d4a3a7fb3d --- /dev/null +++ b/python/servo/try_parser.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python + +# Copyright 2023 The Servo Project Developers. See the COPYRIGHT +# file at the top-level directory of this distribution. +# +# Licensed under the Apache License, Version 2.0 or the MIT license +# , at your +# option. This file may not be copied, modified, or distributed +# except according to those terms. + +import json +import sys +from typing import Optional +import unittest +import logging + +from dataclasses import dataclass +from enum import Enum, Flag, auto + + +class Layout(Flag): + none = 0 + layout2013 = auto() + layout2020 = auto() + + @staticmethod + def all(): + return Layout.layout2013 | Layout.layout2020 + + def to_string(self): + if Layout.all() in self: + return "all" + elif Layout.layout2020 in self: + return "2020" + elif Layout.layout2013 in self: + return "2013" + else: + return "none" + + +class Workflow(str, Enum): + LINUX = 'linux' + MACOS = 'macos' + WINDOWS = 'windows' + ANDROID = 'android' + + +@dataclass +class JobConfig(object): + name: str + workflow: Workflow = Workflow.LINUX + wpt_layout: Layout = Layout.none + profile: str = "release" + unit_tests: bool = False + wpt_tests_to_run: str = "" + + +def handle_preset(s: str) -> Optional[JobConfig]: + s = s.lower() + + if s == "linux": + return JobConfig("Linux", Workflow.LINUX, unit_tests=True) + elif s in ["mac", "macos"]: + return JobConfig("MacOS", Workflow.MACOS, unit_tests=True) + elif s in ["win", "windows"]: + return JobConfig("Windows", Workflow.WINDOWS, unit_tests=True) + elif s in ["wpt", "linux-wpt"]: + return JobConfig("Linux WPT", Workflow.LINUX, unit_tests=True, wpt_layout=Layout.all()) + elif s in ["wpt-2013", "linux-wpt-2013"]: + return JobConfig("Linux WPT legacy-layout", Workflow.LINUX, wpt_layout=Layout.layout2013) + elif s in ["wpt-2020", "linux-wpt-2020"]: + return JobConfig("Linux WPT layout-2020", Workflow.LINUX, wpt_layout=Layout.layout2020) + elif s in ["mac-wpt", "wpt-mac"]: + return JobConfig("MacOS WPT", Workflow.MACOS, wpt_layout=Layout.all()) + elif s == "mac-wpt-2013": + return JobConfig("MacOS WPT legacy-layout", Workflow.MACOS, wpt_layout=Layout.layout2013) + elif s == "mac-wpt-2020": + return JobConfig("MacOS WPT layout-2020", Workflow.MACOS, wpt_layout=Layout.layout2020) + elif s == "android": + return JobConfig("Android", Workflow.ANDROID) + elif s == "webgpu": + return JobConfig("WebGPU CTS", Workflow.LINUX, + wpt_layout=Layout.layout2020, # reftests are mode for new layout + wpt_tests_to_run="_webgpu", # run only webgpu cts + profile="production", # WebGPU works to slow with debug assert + unit_tests=False) # production profile does not work with unit-tests + else: + return None + + +class Encoder(json.JSONEncoder): + def default(self, o): + if isinstance(o, (Config, JobConfig)): + return o.__dict__ + if isinstance(o, Layout): + return o.to_string() + return json.JSONEncoder.default(self, o) + + +class Config(object): + def __init__(self, s: Optional[str] = None): + self.fail_fast: bool = False + self.matrix: list[JobConfig] = list() + if s is not None: + self.parse(s) + + def parse(self, input: str): + input = input.lower().strip() + + if not input: + input = "full" + + words: list[str] = input.split(" ") + + for word in words: + # Handle keywords. + if word in ["fail-fast", "failfast", "fail_fast"]: + self.fail_fast = True + continue # skip over keyword + if word == "full": + words.extend(["linux-wpt", "macos", "windows", "android"]) + continue # skip over keyword + + preset = handle_preset(word) + if preset is None: + print(f"Ignoring unknown preset {word}") + else: + self.matrix.append(preset) + + def to_json(self) -> str: + return json.dumps(self, cls=Encoder) + + +def main(): + conf = Config(" ".join(sys.argv[1:])) + print(conf.to_json()) + + +if __name__ == "__main__": + main() + + +class TestParser(unittest.TestCase): + def test_string(self): + self.assertDictEqual(json.loads(Config("linux fail-fast").to_json()), + {'fail_fast': True, + 'matrix': [{ + 'name': 'Linux', + 'profile': 'release', + 'unit_tests': True, + 'workflow': 'linux', + 'wpt_layout': 'none', + 'wpt_tests_to_run': '' + }] + }) + + def test_empty(self): + self.assertDictEqual(json.loads(Config("").to_json()), + {"fail_fast": False, "matrix": [ + { + "name": "Linux WPT", + "workflow": "linux", + "wpt_layout": "all", + "profile": "release", + "unit_tests": True, + "wpt_tests_to_run": "" + }, + { + "name": "MacOS", + "workflow": "macos", + "wpt_layout": "none", + "profile": "release", + "unit_tests": True, + "wpt_tests_to_run": "" + }, + { + "name": "Windows", + "workflow": "windows", + "wpt_layout": "none", + "profile": "release", + "unit_tests": True, + "wpt_tests_to_run": "" + }, + { + "name": "Android", + "workflow": "android", + "wpt_layout": "none", + "profile": "release", + "unit_tests": False, + "wpt_tests_to_run": "" + } + ]}) + + def test_full(self): + self.assertDictEqual(json.loads(Config("linux-wpt macos windows android").to_json()), + json.loads(Config("").to_json())) + + +def run_tests(): + verbosity = 1 if logging.getLogger().level >= logging.WARN else 2 + suite = unittest.TestLoader().loadTestsFromTestCase(TestParser) + return unittest.TextTestRunner(verbosity=verbosity).run(suite).wasSuccessful()