From 49d097cd54de911a78ec61df599196f656d75e7b Mon Sep 17 00:00:00 2001 From: Ben Hearsum Date: Mon, 17 Nov 2025 11:11:04 -0500 Subject: [PATCH 1/3] fix: skip tests that currently fail on Windows A number of tests currently fail on Windows. Some of them seem like they'll never work, while we may able to get some of them passing with some work. In the short term, just mark them as skipped to allow Windows in CI to land. --- test/conftest.py | 7 +++++++ test/test_config.py | 3 +++ test/test_docker.py | 19 +++++++++++++++++++ test/test_scripts_run_task.py | 3 +++ test/test_transforms_run_toolchain.py | 3 +++ test/test_util_archive.py | 7 +++++++ test/test_util_docker.py | 15 +++++++++++++++ test/test_util_vcs.py | 4 ++++ 8 files changed, 61 insertions(+) diff --git a/test/conftest.py b/test/conftest.py index 3e0714e0b..0a3dcb693 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,4 +1,5 @@ import os +import platform from pathlib import Path import pytest @@ -16,6 +17,12 @@ # no way to ignore ~/.gitconfig other than overriding $HOME, which is overkill. os.environ["GIT_CONFIG_NOSYSTEM"] = "1" +# Some of the tests marked with this may be fixable on Windows; we should +# look into these in more depth at some point. +nowin = pytest.mark.skipif( + platform.system() == "Windows", reason="test currently broken on Windows" +) + @pytest.fixture def responses(): diff --git a/test/test_config.py b/test/test_config.py index 4392e35ba..9a2e941a0 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -10,6 +10,8 @@ from taskgraph.config import GraphConfig +from .conftest import nowin + def test_graph_config_basic(): graph_config = GraphConfig({"foo": "bar"}, "root") @@ -50,6 +52,7 @@ def test_vcs_root_with_non_standard_dir(): assert vcs_root == expected_path +@nowin def test_vcs_root_fallback(mocker): mocker.patch("os.getcwd", return_value="/path/to/repo") diff --git a/test/test_docker.py b/test/test_docker.py index 2e4003c45..c75957bbf 100644 --- a/test/test_docker.py +++ b/test/test_docker.py @@ -10,6 +10,8 @@ from taskgraph.transforms.docker_image import IMAGE_BUILDER_IMAGE from taskgraph.util.vcs import get_repository +from .conftest import nowin + @pytest.fixture def root_url(): @@ -112,6 +114,7 @@ def test_load_task_invalid_task(run_load_task): assert run_load_task(task)[0] == 1 +@nowin def test_load_task(run_load_task): image_task_id = "def" task = { @@ -172,6 +175,7 @@ def test_load_task(run_load_task): assert exp == actual[i] +@nowin def test_load_task_env_init_and_remove(mocker, run_load_task): # Mock NamedTemporaryFile to capture what's written to it mock_envfile = mocker.MagicMock() @@ -252,6 +256,7 @@ def test_load_task_env_init_and_remove(mocker, run_load_task): assert actual[6:8] == ["-v", "/tmp/test_initfile:/builds/worker/.bashrc"] +@nowin @pytest.mark.parametrize( "image", [ @@ -292,6 +297,7 @@ def test_load_task_with_different_image_types( mocks["load_image_by_task_id"].assert_called_once_with(image_task_id) +@nowin def test_load_task_with_local_image( mocker, run_load_task, @@ -322,6 +328,7 @@ def test_load_task_with_local_image( assert mocks["build_image"].call_args[0][1] == "hello-world" +@nowin def test_load_task_with_unsupported_image_type(caplog, run_load_task): caplog.set_level(logging.DEBUG) task = { @@ -343,6 +350,7 @@ def test_load_task_with_unsupported_image_type(caplog, run_load_task): assert "Tasks with unsupported-type images are not supported!" in caplog.text +@nowin def test_load_task_with_task_definition(run_load_task, caplog): # Test passing a task definition directly instead of a task ID caplog.set_level(logging.INFO) @@ -372,6 +380,7 @@ def test_load_task_with_task_definition(run_load_task, caplog): assert "Loading 'test-task-direct' from provided definition" in caplog.text +@nowin def test_load_task_with_interactive_false(run_load_task): # Test non-interactive mode that doesn't require run-task # Task that doesn't use run-task (would fail in interactive mode) @@ -427,6 +436,7 @@ def task(): } +@nowin def test_load_task_with_custom_image_in_tree(run_load_task, task): image = "hello-world" ret, mocks = run_load_task(task, custom_image=image) @@ -453,6 +463,7 @@ def test_load_task_with_custom_image_in_tree(run_load_task, task): assert tag == f"taskcluster/{image}:latest" +@nowin def test_load_task_with_custom_image_task_id(run_load_task, task): image = "task-id=abc" ret, mocks = run_load_task(task, custom_image=image) @@ -460,6 +471,7 @@ def test_load_task_with_custom_image_task_id(run_load_task, task): mocks["load_image_by_task_id"].assert_called_once_with("abc") +@nowin def test_load_task_with_custom_image_index(mocker, run_load_task, task): image = "index=abc" mocker.patch.object(docker, "find_task_id", return_value="abc") @@ -468,6 +480,7 @@ def test_load_task_with_custom_image_index(mocker, run_load_task, task): mocks["load_image_by_task_id"].assert_called_once_with("abc") +@nowin def test_load_task_with_custom_image_registry(mocker, run_load_task, task): image = "ubuntu:latest" ret, mocks = run_load_task(task, custom_image=image) @@ -476,6 +489,7 @@ def test_load_task_with_custom_image_registry(mocker, run_load_task, task): assert not mocks["build_image"].called +@nowin def test_load_task_with_develop(mocker, run_load_task, task): repo_name = "foo" repo_path = "/workdir/vcs" @@ -623,6 +637,7 @@ def mock_path_constructor(path_arg): return inner +@nowin def test_build_image(run_build_image): # Test building image without save_image result, mocks = run_build_image("hello-world") @@ -654,6 +669,7 @@ def test_build_image(run_build_image): assert result == "hello-world:latest" +@nowin def test_build_image_with_parent(mocker, responses, root_url, run_build_image): parent_task_id = "abc" responses.get(f"{root_url}/api/queue/v1/task/{parent_task_id}/status") @@ -687,6 +703,7 @@ def test_build_image_with_parent(mocker, responses, root_url, run_build_image): assert docker_load_args[:3] == ["docker", "load", "-i"] +@nowin def test_build_image_with_parent_not_found( mocker, responses, root_url, run_build_image ): @@ -725,6 +742,7 @@ def test_build_image_with_parent_not_found( assert docker_load_args[:3] == ["docker", "load", "-i"] +@nowin def test_build_image_with_save_image(run_build_image): save_path = "/path/to/save.tar" @@ -741,6 +759,7 @@ def test_build_image_with_save_image(run_build_image): assert save_path in str(result) +@nowin def test_build_image_context_only(run_build_image): context_path = "/path/to/context.tar" diff --git a/test/test_scripts_run_task.py b/test/test_scripts_run_task.py index 933e7b2e3..83fd4e7b7 100644 --- a/test/test_scripts_run_task.py +++ b/test/test_scripts_run_task.py @@ -15,6 +15,8 @@ import taskgraph from taskgraph.util.caches import CACHES +from .conftest import nowin + @pytest.fixture(scope="module") def run_task_mod(): @@ -617,6 +619,7 @@ def inner(extra_args=None, env=None): return inner +@nowin def test_main_abspath_environment(mocker, run_main): envvars = ["GECKO_PATH", "MOZ_FETCHES_DIR", "UPLOAD_DIR"] envvars += [cache["env"] for cache in CACHES.values() if "env" in cache] diff --git a/test/test_transforms_run_toolchain.py b/test/test_transforms_run_toolchain.py index 3ec843dff..46554ea00 100644 --- a/test/test_transforms_run_toolchain.py +++ b/test/test_transforms_run_toolchain.py @@ -11,6 +11,8 @@ from taskgraph.util.templates import merge from taskgraph.util.vcs import GitRepository +from .conftest import nowin + TASK_DEFAULTS = { "description": "fake description", "label": "fake-task-label", @@ -205,6 +207,7 @@ def assert_relative_script(task, taskdesc): assert_docker_worker(task, taskdesc) +@nowin @pytest.mark.parametrize( "task", ( diff --git a/test/test_util_archive.py b/test/test_util_archive.py index ee256214b..6b1998a2e 100644 --- a/test/test_util_archive.py +++ b/test/test_util_archive.py @@ -15,6 +15,8 @@ create_tar_gz_from_files, ) +from .conftest import nowin + MODE_STANDARD = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH @@ -72,6 +74,7 @@ def test_dirs_refused(tmp_path): create_tar_from_files(fh, {"test": str(tmp_path)}) +@nowin def test_setuid_setgid_refused(tmp_path): uid = tmp_path / "setuid" uid.touch() @@ -89,6 +92,7 @@ def test_setuid_setgid_refused(tmp_path): create_tar_from_files(fh, {"test": str(gid)}) +@nowin def test_create_tar_basic(tmp_path, create_files): files = create_files() @@ -103,6 +107,7 @@ def test_create_tar_basic(tmp_path, create_files): verify_basic_tarfile(tf) +@nowin def test_executable_preserved(tmp_path): p = tmp_path / "exec" p.write_bytes(b"#!/bin/bash\n") @@ -129,6 +134,7 @@ def test_executable_preserved(tmp_path): assert extracted_content == b"#!/bin/bash\n" +@nowin def test_create_tar_gz_basic(tmp_path, create_files): gp = tmp_path / "test.tar.gz" with open(gp, "wb") as fh: @@ -156,6 +162,7 @@ def test_create_tar_gz_basic(tmp_path, create_files): verify_basic_tarfile(tf) +@nowin def test_tar_gz_name(tmp_path, create_files): gp = tmp_path / "test.tar.gz" with open(gp, "wb") as fh: diff --git a/test/test_util_docker.py b/test/test_util_docker.py index f140b1668..98ad607cd 100644 --- a/test/test_util_docker.py +++ b/test/test_util_docker.py @@ -17,6 +17,7 @@ from taskgraph.config import GraphConfig from taskgraph.util import docker +from .conftest import nowin from .mockedopen import MockedOpen MODE_STANDARD = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH @@ -24,6 +25,7 @@ @mock.patch.dict("os.environ", {"TASKCLUSTER_ROOT_URL": liburls.test_root_url()}) class TestDocker(unittest.TestCase): + @nowin def test_generate_context_hash(self): tmpdir = tempfile.mkdtemp() try: @@ -45,6 +47,7 @@ def test_generate_context_hash(self): finally: shutil.rmtree(tmpdir) + @nowin def test_docker_image_explicit_registry(self): files = {} files[f"{docker.IMAGE_DIR}/myimage/REGISTRY"] = "cool-images" @@ -55,6 +58,7 @@ def test_docker_image_explicit_registry(self): docker.docker_image("myimage"), "cool-images/myimage@sha256:434..." ) + @nowin def test_docker_image_explicit_registry_by_tag(self): files = {} files[f"{docker.IMAGE_DIR}/myimage/REGISTRY"] = "myreg" @@ -65,6 +69,7 @@ def test_docker_image_explicit_registry_by_tag(self): docker.docker_image("myimage", by_tag=True), "myreg/myimage:1.2.3" ) + @nowin def test_docker_image_default_registry(self): files = {} files[f"{docker.IMAGE_DIR}/REGISTRY"] = "mozilla" @@ -75,6 +80,7 @@ def test_docker_image_default_registry(self): docker.docker_image("myimage"), "mozilla/myimage@sha256:434..." ) + @nowin def test_docker_image_default_registry_by_tag(self): files = {} files[f"{docker.IMAGE_DIR}/REGISTRY"] = "mozilla" @@ -85,6 +91,7 @@ def test_docker_image_default_registry_by_tag(self): docker.docker_image("myimage", by_tag=True), "mozilla/myimage:1.2.3" ) + @nowin def test_create_context_tar_basic(self): tmp = tempfile.mkdtemp() try: @@ -116,6 +123,7 @@ def test_create_context_tar_basic(self): finally: shutil.rmtree(tmp) + @nowin def test_create_context_topsrcdir_files(self): tmp = tempfile.mkdtemp() try: @@ -148,6 +156,7 @@ def test_create_context_topsrcdir_files(self): finally: shutil.rmtree(tmp) + @nowin def test_create_context_absolute_path(self): tmp = tempfile.mkdtemp() try: @@ -163,6 +172,7 @@ def test_create_context_absolute_path(self): finally: shutil.rmtree(tmp) + @nowin def test_create_context_outside_topsrcdir(self): tmp = tempfile.mkdtemp() try: @@ -177,6 +187,7 @@ def test_create_context_outside_topsrcdir(self): finally: shutil.rmtree(tmp) + @nowin def test_create_context_missing_extra(self): tmp = tempfile.mkdtemp() try: @@ -191,6 +202,7 @@ def test_create_context_missing_extra(self): finally: shutil.rmtree(tmp) + @nowin def test_create_context_extra_directory(self): tmp = tempfile.mkdtemp() try: @@ -235,6 +247,7 @@ def test_create_context_extra_directory(self): finally: shutil.rmtree(tmp) + @nowin def test_stream_context_tar(self): tmp = tempfile.mkdtemp() try: @@ -271,6 +284,7 @@ def test_stream_context_tar(self): finally: shutil.rmtree(tmp) + @nowin def test_image_paths_with_custom_kind(self): """Test image_paths function with graph_config parameter.""" temp_dir = tempfile.mkdtemp() @@ -309,6 +323,7 @@ def test_image_paths_with_custom_kind(self): finally: shutil.rmtree(temp_dir) + @nowin def test_parse_volumes_with_graph_config(self): """Test parse_volumes function with graph_config parameter.""" temp_dir = tempfile.mkdtemp() diff --git a/test/test_util_vcs.py b/test/test_util_vcs.py index 2e2327342..e68821bce 100644 --- a/test/test_util_vcs.py +++ b/test/test_util_vcs.py @@ -11,6 +11,8 @@ from taskgraph.util.vcs import HgRepository, Repository, get_repository +from .conftest import nowin + def test_get_repository(repo): r = get_repository(repo.path) @@ -215,6 +217,7 @@ def assert_files(actual, expected): assert set(actual) == set(expected) +@nowin def test_get_tracked_files(repo): assert_files(repo.get_tracked_files(), ["first_file"]) @@ -370,6 +373,7 @@ def test_get_changed_files_two_revisions(repo): ) +@nowin def test_workdir_outgoing(repo_with_upstream): repo, upstream_location = repo_with_upstream From 2e363e9c8119afca263006e578dcb6a8782e5317 Mon Sep 17 00:00:00 2001 From: Ben Hearsum Date: Tue, 28 Oct 2025 17:24:25 -0400 Subject: [PATCH 2/3] feat: run unit tests on Windows --- taskcluster/kinds/test/kind.yml | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/taskcluster/kinds/test/kind.yml b/taskcluster/kinds/test/kind.yml index a0833dbda..95adbfc76 100644 --- a/taskcluster/kinds/test/kind.yml +++ b/taskcluster/kinds/test/kind.yml @@ -18,18 +18,14 @@ task-defaults: artifact_prefix: public retrigger: true code-review: true - worker-type: t-linux worker: - docker-image: {in-tree: python} max-run-time: 1800 env: - LC_ALL: "C" PYTHONUTF8: "0" PYTHONCOERCECLOCALE: "0" HGENCODING: "utf-8" treeherder: kind: test - platform: test/opt tier: 1 run: using: run-task @@ -37,23 +33,44 @@ task-defaults: use-caches: [checkout, uv] tasks: - unit: - description: "Run unit tests with py{matrix[python]}" + unit-linux: + description: "Run unit tests with py{matrix[python]} on Linux" matrix: set-name: "unit-py{matrix[python]}" substitution-fields: [description, run.command, treeherder, worker, attributes] python: ["314t", "314", "313", "312", "311", "310", "39"] attributes: python: "{matrix[python]}" + platform: linux + worker-type: t-linux worker: + docker-image: {in-tree: python} artifacts: - type: file path: "/builds/worker/artifacts/coverage" name: "public/coverage.py{matrix[python]}" env: + LC_ALL: "C" UV_PYTHON: "{matrix[python]}" treeherder: + platform: test-linux/opt symbol: unit(py{matrix[python]}) run: command: >- uv run coverage run --data-file /builds/worker/artifacts/coverage --context=py{matrix[python]} -m pytest -vv + + unit-win: + description: "Run unit tests with python on Windows" + attributes: + platform: windows + worker-type: t-windows + worker: + taskcluster-proxy: true + treeherder: + platform: test-win/opt + symbol: unit(py) + run: + command: >- + python3 -V && + powershell -ExecutionPolicy ByPass -c 'irm https://astral.sh/uv/install.ps1 | iex' && + ~/.local/bin/uv.exe run -m pytest -vv From a0402130462d18a12576b71dfc069d23cf64f048 Mon Sep 17 00:00:00 2001 From: Ben Hearsum Date: Tue, 28 Oct 2025 17:36:02 -0400 Subject: [PATCH 3/3] feat: add windows worker pool --- taskcluster/config.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/taskcluster/config.yml b/taskcluster/config.yml index 4e47debc8..d9d32aac2 100644 --- a/taskcluster/config.yml +++ b/taskcluster/config.yml @@ -38,3 +38,8 @@ workers: implementation: docker-worker os: linux worker-type: linux-docker + t-windows: + provisioner: 'taskgraph-t' + implementation: generic-worker + os: windows + worker-type: win11-64-24h2