diff --git a/.circleci/config.yml b/.circleci/config.yml index 0c831af04..d327ecbc7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -24,6 +24,11 @@ version: 2.1 dockerhub-context-template: &DOCKERHUB_CONTEXT context: "dockerhub-auth" +# Required environment for using the coveralls tool to upload partial coverage +# reports and then finish the process. +coveralls-environment: &COVERALLS_ENVIRONMENT + COVERALLS_REPO_TOKEN: "JPf16rLB7T2yjgATIxFzTsEgMdN1UNq6o" + # Next is a Docker executor template that gets the credentials from the # environment and supplies them to the executor. dockerhub-auth-template: &DOCKERHUB_AUTH @@ -112,6 +117,21 @@ workflows: - "another-locale": {} + - "windows-server-2022": + name: "Windows Server 2022, CPython <>" + matrix: + parameters: + # Run the job for a number of CPython versions. These are the + # two versions installed on the version of the Windows VM image + # we specify (in the executor). This is handy since it means we + # don't have to do any Python installation work. We pin the + # Windows VM image so these shouldn't shuffle around beneath us + # but if we want to update that image or get different versions + # of Python, we probably have to do something here. + pythonVersion: + - "3.9" + - "3.11" + - "integration": # Run even the slow integration tests here. We need the `--` to # sneak past tox and get to pytest. @@ -126,6 +146,15 @@ workflows: - "docs": {} + - "finish-coverage-report": + requires: + # Referencing the job by "alias" (as CircleCI calls the mapping + # key) instead of the value of its "name" property causes us to + # require every instance of the job from its matrix expansion. So + # this requirement is enough to require every Windows Server 2022 + # job. + - "windows-server-2022" + images: <<: *IMAGES @@ -133,6 +162,20 @@ workflows: when: "<< pipeline.parameters.build-images >>" jobs: + finish-coverage-report: + docker: + - <<: *DOCKERHUB_AUTH + image: "python:3-slim" + + steps: + - run: + name: "Indicate completion to coveralls.io" + environment: + <<: *COVERALLS_ENVIRONMENT + command: | + pip install coveralls==3.3.1 + python -m coveralls --finish + codechecks: docker: - <<: *DOCKERHUB_AUTH @@ -151,6 +194,161 @@ jobs: command: | ~/.local/bin/tox -e codechecks + windows-server-2022: + parameters: + pythonVersion: + description: >- + An argument to pass to the `py` launcher to choose a Python version. + type: "string" + default: "" + + executor: "windows" + environment: + # Tweak Hypothesis to make its behavior more suitable for the CI + # environment. This should improve reproducibility and lessen the + # effects of variable compute resources. + TAHOE_LAFS_HYPOTHESIS_PROFILE: "ci" + + # Tell pip where its download cache lives. This must agree with the + # "save_cache" step below or caching won't really work right. + PIP_CACHE_DIR: "pip-cache" + + # And tell pip where it can find out cached wheelhouse for fast wheel + # installation, even for projects that don't distribute wheels. This + # must also agree with the "save_cache" step below. + PIP_FIND_LINKS: "wheelhouse" + + steps: + - "checkout" + + # If possible, restore a pip download cache to save us from having to + # download all our Python dependencies from PyPI. + - "restore_cache": + keys: + # The download cache and/or the wheelhouse may contain Python + # version-specific binary packages so include the Python version + # in this key, as well as the canonical source of our + # dependencies. + - &CACHE_KEY "pip-packages-v1-<< parameters.pythonVersion >>-{{ checksum \"setup.py\" }}" + + - "run": + name: "Fix $env:PATH" + command: | + # The Python this job is parameterized is not necessarily the one + # at the front of $env:PATH. Modify $env:PATH so that it is so we + # can just say "python" in the rest of the steps. Also get the + # related Scripts directory so tools from packages we install are + # also available. + $p = py -<> -c "import sys; print(sys.prefix)" + $q = py -<> -c "import sysconfig; print(sysconfig.get_path('scripts'))" + + New-Item $Profile.CurrentUserAllHosts -Force + # $p gets "python" on PATH and $q gets tools from packages we + # install. Note we carefully construct the string so that + # $env:PATH is not substituted now but $p and $q are. ` is the + # PowerShell string escape character. + Add-Content -Path $Profile.CurrentUserAllHosts -Value "`$env:PATH = `"$p;$q;`$env:PATH`"" + + - "run": + name: "Display tool versions" + command: | + python misc/build_helpers/show-tool-versions.py + + - "run": + # It's faster to install a wheel than a source package. If we don't + # have a cached wheelhouse then build all of the wheels and dump + # them into a directory where they can become a cached wheelhouse. + # We would have built these wheels during installation anyway so it + # doesn't cost us anything extra and saves us effort next time. + name: "(Maybe) Build Wheels" + command: | + if ((Test-Path .\wheelhouse) -and (Test-Path .\wheelhouse\*)) { + echo "Found populated wheelhouse, skipping wheel building." + } else { + python -m pip install wheel + python -m pip wheel --wheel-dir $env:PIP_FIND_LINKS .[testenv] .[test] + } + + - "save_cache": + paths: + # Make sure this agrees with PIP_CACHE_DIR in the environment. + - "pip-cache" + - "wheelhouse" + key: *CACHE_KEY + + - "run": + name: "Install Dependencies" + environment: + # By this point we should no longer need an index. + PIP_NO_INDEX: "1" + command: | + python -m pip install .[testenv] .[test] + + - "run": + name: "Run Unit Tests" + environment: + # Configure the results location for the subunitv2-file reporter + # from subunitreporter + SUBUNITREPORTER_OUTPUT_PATH: "test-results.subunit2" + + # Try to get prompt output from the reporter to avoid no-output + # timeouts. + PYTHONUNBUFFERED: "1" + + command: | + # Run the test suite under coverage measurement using the + # parameterized version of Python, writing subunitv2-format + # results to the file given in the environment. + python -b -m coverage run -m twisted.trial --reporter=subunitv2-file --rterrors allmydata + + - "run": + name: "Upload Coverage" + environment: + <<: *COVERALLS_ENVIRONMENT + # Mark the data as just one piece of many because we have more + # than one instance of this job (two on Windows now, some on other + # platforms later) which collects and reports coverage. This is + # necessary to cause Coveralls to merge multiple coverage results + # into a single report. Note the merge only happens when we + # "finish" a particular build, as identified by its "build_num" + # (aka "service_number"). + COVERALLS_PARALLEL: "true" + command: | + python -m pip install coveralls==3.3.1 + + # .coveragerc sets parallel = True so we don't have a `.coverage` + # file but a `.coverage.` file (or maybe more than + # one, but probably not). coveralls can't work with these so + # merge them before invoking it. + python -m coverage combine + + # Now coveralls will be able to find the data, so have it do the + # upload. Also, have it strip the system config-specific prefix + # from all of the source paths. + $prefix = python -c "import sysconfig; print(sysconfig.get_path('purelib'))" + python -m coveralls --basedir $prefix + + - "run": + name: "Convert Result Log" + command: | + # subunit2junitxml exits with error if the result stream it is + # converting has test failures in it! So this step might fail. + # Since the step in which we actually _ran_ the tests won't fail + # even if there are test failures, this is a good thing for now. + subunit2junitxml.exe --output-to=test-results.xml test-results.subunit2 + + - "store_test_results": + path: "test-results.xml" + + - "store_artifacts": + path: "_trial_temp/test.log" + + - "store_artifacts": + path: "eliot.log" + + - "store_artifacts": + path: ".coverage" + pyinstaller: docker: - <<: *DOCKERHUB_AUTH @@ -527,6 +725,15 @@ jobs: # PYTHON_VERSION: "2" executors: + windows: + # Choose a Windows environment that closest matches our testing + # requirements and goals. + # https://circleci.com/developer/orbs/orb/circleci/windows#executors-server-2022 + machine: + image: "windows-server-2022-gui:2023.06.1" + shell: "powershell.exe -ExecutionPolicy Bypass" + resource_class: "windows.large" + nix: docker: # Run in a highly Nix-capable environment. diff --git a/.coveragerc b/.coveragerc index d09554cad..5b41f9ce3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -19,7 +19,7 @@ skip_covered = True source = # It looks like this in the checkout src/ -# It looks like this in the Windows build environment +# It looks like this in the GitHub Actions Windows build environment D:/a/tahoe-lafs/tahoe-lafs/.tox/py*-coverage/Lib/site-packages/ # Although sometimes it looks like this instead. Also it looks like this on macOS. .tox/py*-coverage/lib/python*/site-packages/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d3862ffad..0f38b0291 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,13 +44,6 @@ jobs: strategy: fail-fast: false matrix: - os: - - windows-latest - python-version: - - "3.8" - - "3.9" - - "3.10" - - "3.11" include: # On macOS don't bother with 3.8, just to get faster builds. - os: macos-12 diff --git a/newsfragments/4059.minor b/newsfragments/4059.minor new file mode 100644 index 000000000..e69de29bb diff --git a/setup.py b/setup.py index 86873ad53..433721d2a 100644 --- a/setup.py +++ b/setup.py @@ -413,7 +413,7 @@ setup(name="tahoe-lafs", # also set in __init__.py "pip==22.0.3", "wheel==0.37.1", "setuptools==60.9.1", - "subunitreporter==22.2.0", + "subunitreporter==23.8.0", "python-subunit==1.4.2", "junitxml==0.7", "coverage==7.2.5", diff --git a/src/allmydata/test/cli/test_grid_manager.py b/src/allmydata/test/cli/test_grid_manager.py index 604cd6b7b..b44b322d2 100644 --- a/src/allmydata/test/cli/test_grid_manager.py +++ b/src/allmydata/test/cli/test_grid_manager.py @@ -23,6 +23,9 @@ import click.testing from ..common_util import ( run_cli, ) +from ..common import ( + superuser, +) from twisted.internet.defer import ( inlineCallbacks, ) @@ -34,7 +37,6 @@ from twisted.python.runtime import ( ) from allmydata.util import jsonbytes as json - class GridManagerCommandLine(TestCase): """ Test the mechanics of the `grid-manager` command @@ -223,7 +225,7 @@ class GridManagerCommandLine(TestCase): ) @skipIf(platform.isWindows(), "We don't know how to set permissions on Windows.") - @skipIf(os.getuid() == 0, "cannot test as superuser with all permissions") + @skipIf(superuser, "cannot test as superuser with all permissions") def test_sign_bad_perms(self): """ Error reported if we can't create certificate file diff --git a/src/allmydata/test/common.py b/src/allmydata/test/common.py index db2921e86..d61bc28f1 100644 --- a/src/allmydata/test/common.py +++ b/src/allmydata/test/common.py @@ -117,6 +117,10 @@ from subprocess import ( PIPE, ) +# Is the process running as an OS user with elevated privileges (ie, root)? +# We only know how to determine this for POSIX systems. +superuser = getattr(os, "getuid", lambda: -1)() == 0 + EMPTY_CLIENT_CONFIG = config_from_string( "/dev/null", "tub.port", diff --git a/src/allmydata/test/test_client.py b/src/allmydata/test/test_client.py index 86c95a310..c0cce2809 100644 --- a/src/allmydata/test/test_client.py +++ b/src/allmydata/test/test_client.py @@ -77,6 +77,7 @@ from allmydata.scripts.common import ( from foolscap.api import flushEventualQueue import allmydata.test.common_util as testutil from .common import ( + superuser, EMPTY_CLIENT_CONFIG, SyncTestCase, AsyncBrokenTestCase, @@ -151,7 +152,7 @@ class Basic(testutil.ReallyEqualMixin, unittest.TestCase): # EnvironmentError when reading a file that really exists), on # windows, please fix this @skipIf(platform.isWindows(), "We don't know how to set permissions on Windows.") - @skipIf(os.getuid() == 0, "cannot test as superuser with all permissions") + @skipIf(superuser, "cannot test as superuser with all permissions") def test_unreadable_config(self): basedir = "test_client.Basic.test_unreadable_config" os.mkdir(basedir) diff --git a/src/allmydata/test/test_node.py b/src/allmydata/test/test_node.py index 1469ec5b2..90da877fb 100644 --- a/src/allmydata/test/test_node.py +++ b/src/allmydata/test/test_node.py @@ -62,6 +62,7 @@ from .common import ( ConstantAddresses, SameProcessStreamEndpointAssigner, UseNode, + superuser, ) def port_numbers(): @@ -325,7 +326,7 @@ class TestCase(testutil.SignalMixin, unittest.TestCase): self.assertEqual(config.items("nosuch", default), default) @skipIf(platform.isWindows(), "We don't know how to set permissions on Windows.") - @skipIf(os.getuid() == 0, "cannot test as superuser with all permissions") + @skipIf(superuser, "cannot test as superuser with all permissions") def test_private_config_unreadable(self): """ Asking for inaccessible private config is an error @@ -341,7 +342,7 @@ class TestCase(testutil.SignalMixin, unittest.TestCase): config.get_or_create_private_config("foo") @skipIf(platform.isWindows(), "We don't know how to set permissions on Windows.") - @skipIf(os.getuid() == 0, "cannot test as superuser with all permissions") + @skipIf(superuser, "cannot test as superuser with all permissions") def test_private_config_unreadable_preexisting(self): """ error if reading private config data fails @@ -398,7 +399,7 @@ class TestCase(testutil.SignalMixin, unittest.TestCase): self.assertEqual(len(counter), 1) # don't call unless necessary self.assertEqual(value, "newer") - @skipIf(os.getuid() == 0, "cannot test as superuser with all permissions") + @skipIf(superuser, "cannot test as superuser with all permissions") def test_write_config_unwritable_file(self): """ Existing behavior merely logs any errors upon writing