Repository: thoth-station/micropipenv (language: python)

Issue:
Windows in the CI
As it was uncovered to me in #162 , micropipenv has some users on Windows so it might be a good idea to add Windows to our CI.

The configuration is pretty simple - WIP in https://github.com/frenzymadness/micropipenv/tree/windows_ci - the only remaining thing is to find a way how to run different Python versions in parallel.

But it seems that the pytest-venv is not compatible with Windows. It should be easy to fix but I have to find a Windows machine to work on or somebody who can do it for us.


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 230c0c1..fc2f61d 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -7,13 +7,14 @@ on:
     branches:
     - master
 
-name: Run Tox tests
+name: Run Tox tests on Linux
 
 jobs:
   tox_test:
     name: Tox test
     steps:
-    - uses: actions/checkout@v2
+    - name: Checkout micropipenv
+      uses: actions/checkout@v2
     - name: Run Tox tests
       id: test
       uses: fedora-python/tox-github-action@master
diff --git a/.github/workflows/prep_windows_matrix.py b/.github/workflows/prep_windows_matrix.py
new file mode 100755
index 0000000..6d8cb9d
--- /dev/null
+++ b/.github/workflows/prep_windows_matrix.py
@@ -0,0 +1,15 @@
+#!/usr/bin/python3
+
+"""Helper script for Github CI matrix definition."""
+
+import fileinput
+import re
+
+TEMPLATE = '''- python_version: "{}"
+  tox_env: "{}"'''
+
+for line in fileinput.input():
+    line = line.strip()
+    m = re.match(r"^py(\d)(\d+)-", line)
+    python_version = "{}.{}".format(*m.groups())
+    print(TEMPLATE.format(python_version, line))
diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml
new file mode 100644
index 0000000..bb8fd24
--- /dev/null
+++ b/.github/workflows/windows.yml
@@ -0,0 +1,117 @@
+on:
+  push:
+    branches:
+    - master
+    - ci-test
+  pull_request:
+    branches:
+    - master
+
+name: Run Tox tests on Windows
+
+jobs:
+  tox_test:
+    name: Tox test
+    steps:
+    - name: Checkout micropipenv
+      uses: actions/checkout@v2
+    - name: Set up Python
+      uses: actions/setup-python@v2
+      with:
+        python-version: ${{ matrix.python_version }}
+    - name: Install tox
+      run: python -m pip install tox
+    - name: Run Tox tests
+      id: test
+      run: tox -e ${{ matrix.tox_env }}
+
+    runs-on: windows-latest
+    strategy:
+      matrix:
+        # This has to be manually configured because Windows images contains
+        # more than just the main Python and there is no easy way how to tell
+        # tox to ignore the non-main interpreters.
+        # The list can be obtained by `tox -l | .github/workflows/prep_windows_matrix.py`
+        include:
+          - python_version: "3.6"
+            tox_env: "py36-pip90-toml"
+          - python_version: "3.6"
+            tox_env: "py36-pip191-toml"
+          - python_version: "3.6"
+            tox_env: "py36-pip192-toml"
+          - python_version: "3.6"
+            tox_env: "py36-pip193-toml"
+          - python_version: "3.6"
+            tox_env: "py36-pip200-toml"
+          - python_version: "3.6"
+            tox_env: "py36-pip201-toml"
+          - python_version: "3.6"
+            tox_env: "py36-piplatest-toml"
+          - python_version: "3.6"
+            tox_env: "py36-pipgit-toml"
+          - python_version: "3.7"
+            tox_env: "py37-pip90-toml"
+          - python_version: "3.7"
+            tox_env: "py37-pip191-toml"
+          - python_version: "3.7"
+            tox_env: "py37-pip192-toml"
+          - python_version: "3.7"
+            tox_env: "py37-pip193-toml"
+          - python_version: "3.7"
+            tox_env: "py37-pip200-toml"
+          - python_version: "3.7"
+            tox_env: "py37-pip201-toml"
+          - python_version: "3.7"
+            tox_env: "py37-piplatest-toml"
+          - python_version: "3.7"
+            tox_env: "py37-pipgit-toml"
+          - python_version: "3.8"
+            tox_env: "py38-pip90-toml"
+          - python_version: "3.8"
+            tox_env: "py38-pip191-toml"
+          - python_version: "3.8"
+            tox_env: "py38-pip192-toml"
+          - python_version: "3.8"
+            tox_env: "py38-pip193-toml"
+          - python_version: "3.8"
+            tox_env: "py38-pip200-toml"
+          - python_version: "3.8"
+            tox_env: "py38-pip201-toml"
+          - python_version: "3.8"
+            tox_env: "py38-piplatest-toml"
+          - python_version: "3.8"
+            tox_env: "py38-pipgit-toml"
+          - python_version: "3.9"
+            tox_env: "py39-pip90-toml"
+          - python_version: "3.9"
+            tox_env: "py39-pip191-toml"
+          - python_version: "3.9"
+            tox_env: "py39-pip192-toml"
+          - python_version: "3.9"
+            tox_env: "py39-pip193-toml"
+          - python_version: "3.9"
+            tox_env: "py39-pip200-toml"
+          - python_version: "3.9"
+            tox_env: "py39-pip201-toml"
+          - python_version: "3.9"
+            tox_env: "py39-piplatest-toml"
+          - python_version: "3.9"
+            tox_env: "py39-pipgit-toml"
+          - python_version: "3.6"
+            tox_env: "py36-pip90-pytoml"
+          - python_version: "3.6"
+            tox_env: "py36-piplatest-pytoml"
+          - python_version: "3.6"
+            tox_env: "py36-pipgit-pytoml"
+          - python_version: "3.8"
+            tox_env: "py38-pip90-pytoml"
+          - python_version: "3.8"
+            tox_env: "py38-piplatest-pytoml"
+          - python_version: "3.8"
+            tox_env: "py38-pipgit-pytoml"
+          - python_version: "3.9"
+            tox_env: "py39-pip90-pytoml"
+          - python_version: "3.9"
+            tox_env: "py39-piplatest-pytoml"
+          - python_version: "3.9"
+            tox_env: "py39-pipgit-pytoml"


Test patch:
diff --git a/tests/test_micropipenv.py b/tests/test_micropipenv.py
index 50ba4bc..7dc0e21 100644
--- a/tests/test_micropipenv.py
+++ b/tests/test_micropipenv.py
@@ -37,6 +37,9 @@
 _DATA_DIR = os.path.abspath(os.path.join(os.path.dirname(os.path.relpath(__file__)), "data"))
 # Implementation of `toml` to test micropipenv with
 MICROPIPENV_TEST_TOML_MODULE = os.getenv("MICROPIPENV_TEST_TOML_MODULE", "toml")
+WIN = sys.platform == "win32"
+BIN_DIR = "Scripts" if WIN else "bin"
+SP_DIR = ("lib", "site-packages") if WIN else ("lib", "python*", "site-packages")
 
 
 @contextmanager
@@ -52,7 +55,14 @@ def cwd(target):
 
 def get_pip_path(venv):
     """Get path to pip in a virtual environment used in tests."""
-    return os.path.join(venv.path, "bin", "pip3")
+    return os.path.join(venv.path, BIN_DIR, "pip3")
+
+
+def get_updated_env(venv):
+    """Return `os.environ` with added MICROPIPENV_ vars."""
+    new_env = dict(os.environ)
+    new_env.update({"MICROPIPENV_PIP_BIN": get_pip_path(venv), "MICROPIPENV_DEBUG": "1"})
+    return new_env
 
 
 def setup_module():
@@ -94,9 +104,9 @@ def check_generated_pipfile_lock(pipfile_lock_path, pipfile_lock_path_expected):
 @pytest.mark.online
 def test_install_pipenv(venv):
     """Test invoking installation using information in Pipfile.lock."""
-    cmd = [os.path.join(venv.path, "bin", "python3"), micropipenv.__file__, "install", "--method", "pipenv"]
+    cmd = [os.path.join(venv.path, BIN_DIR, "python"), micropipenv.__file__, "install", "--method", "pipenv"]
     with cwd(os.path.join(_DATA_DIR, "install", "pipenv")):
-        subprocess.run(cmd, check=True, env={"MICROPIPENV_PIP_BIN": get_pip_path(venv), "MICROPIPENV_DEBUG": "1"})
+        subprocess.run(cmd, check=True, env=get_updated_env(venv))
         assert str(venv.get_version("daiquiri")) == "2.0.0"
         assert str(venv.get_version("python-json-logger")) == "0.1.11"
 
@@ -104,18 +114,18 @@ def test_install_pipenv(venv):
 @pytest.mark.online
 def test_install_pipenv_vcs(venv):
     """Test invoking installation using information in Pipfile.lock, a git version is used."""
-    cmd = [os.path.join(venv.path, "bin", "python3"), micropipenv.__file__, "install", "--method", "pipenv"]
+    cmd = [os.path.join(venv.path, BIN_DIR, "python"), micropipenv.__file__, "install", "--method", "pipenv"]
     with cwd(os.path.join(_DATA_DIR, "install", "pipenv_vcs")):
-        subprocess.run(cmd, check=True, env={"MICROPIPENV_PIP_BIN": get_pip_path(venv), "MICROPIPENV_DEBUG": "1"})
+        subprocess.run(cmd, check=True, env=get_updated_env(venv))
         assert str(venv.get_version("daiquiri")) == "2.0.0"
 
 
 @pytest.mark.online
 def test_install_pipenv_file(venv):
     """Test invoking installation using information in Pipfile.lock, a file mode is used."""
-    cmd = [os.path.join(venv.path, "bin", "python3"), micropipenv.__file__, "install", "--method", "pipenv"]
+    cmd = [os.path.join(venv.path, BIN_DIR, "python"), micropipenv.__file__, "install", "--method", "pipenv"]
     with cwd(os.path.join(_DATA_DIR, "install", "pipenv_file")):
-        subprocess.run(cmd, check=True, env={"MICROPIPENV_PIP_BIN": get_pip_path(venv), "MICROPIPENV_DEBUG": "1"})
+        subprocess.run(cmd, check=True, env=get_updated_env(venv))
         assert str(venv.get_version("daiquiri")) == "2.0.0"
         assert str(venv.get_version("python-json-logger")) == "0.1.11"
 
@@ -123,46 +133,48 @@ def test_install_pipenv_file(venv):
 @pytest.mark.online
 def test_install_pipenv_editable(venv):
     """Test invoking installation using information in Pipfile.lock, an editable mode is used."""
-    cmd = [os.path.join(venv.path, "bin", "python3"), micropipenv.__file__, "install", "--method", "pipenv"]
+    cmd = [os.path.join(venv.path, BIN_DIR, "python"), micropipenv.__file__, "install", "--method", "pipenv"]
     with cwd(os.path.join(_DATA_DIR, "install", "pipenv_editable")):
         try:
-            subprocess.run(cmd, check=True, env={"MICROPIPENV_PIP_BIN": get_pip_path(venv), "MICROPIPENV_DEBUG": "1"})
+            subprocess.run(cmd, check=True, env=get_updated_env(venv))
             assert str(venv.get_version("daiquiri")) == "2.0.0"
             assert str(venv.get_version("python-json-logger")) == "0.1.11"
             assert str(venv.get_version("micropipenv-editable-test")) == "1.2.3"
             assert (
-                len(
-                    glob.glob(
-                        os.path.join(venv.path, "lib", "python*", "site-packages", "micropipenv-editable-test.egg-link")
-                    )
-                )
-                == 1
+                len(glob.glob(os.path.join(venv.path, *SP_DIR, "micropipenv-editable-test.egg-link"))) == 1
             ), "No egg-link found for editable install"
         finally:
             # Clean up this file, can cause issues across multiple test runs.
             shutil.rmtree("micropipenv_editable_test.egg-info", ignore_errors=True)
 
 
+# This test does not work on Windows because files in the .git folder
+# has some special permissions that the cleanup method of TemporaryDirectory
+# from conftest.pytest_configure cannot delete them.
+# Python 3.10 has an option to ignore errors like this so we can enable it
+# back soon at least for Python 3.10.
+# See: https://github.com/python/cpython/pull/24793
 @pytest.mark.online
+@pytest.mark.skipif(WIN, reason="Fails to remove .git folder on Windows")
 def test_install_pipenv_vcs_editable(venv):
     """Test invoking installation using information in Pipfile.lock, a git version in editable mode is used."""
-    cmd = [os.path.join(venv.path, "bin", "python3"), micropipenv.__file__, "install", "--method", "pipenv"]
+    cmd = [os.path.join(venv.path, BIN_DIR, "python"), micropipenv.__file__, "install", "--method", "pipenv"]
     with cwd(os.path.join(_DATA_DIR, "install", "pipenv_vcs_editable")):
-        subprocess.run(cmd, check=True, env={"MICROPIPENV_PIP_BIN": get_pip_path(venv), "MICROPIPENV_DEBUG": "1"})
+        subprocess.run(cmd, check=True, env=get_updated_env(venv))
         assert str(venv.get_version("daiquiri")) == "1.6.0"
         assert (
-            len(glob.glob(os.path.join(venv.path, "lib", "python*", "site-packages", "daiquiri.egg-link"))) == 1
+            len(glob.glob(os.path.join(venv.path, *SP_DIR, "daiquiri.egg-link"))) == 1
         ), "No egg-link found for editable install"
 
 
 @pytest.mark.online
 def test_install_poetry(venv):
     """Test invoking installation using information from a Poetry project."""
-    cmd = [os.path.join(venv.path, "bin", "python3"), micropipenv.__file__, "install", "--method", "poetry"]
+    cmd = [os.path.join(venv.path, BIN_DIR, "python"), micropipenv.__file__, "install", "--method", "poetry"]
     venv.install(MICROPIPENV_TEST_TOML_MODULE)
     work_dir = os.path.join(_DATA_DIR, "install", "poetry")
     with cwd(os.path.join(_DATA_DIR, "install", "poetry")):
-        subprocess.run(cmd, check=True, env={"MICROPIPENV_PIP_BIN": get_pip_path(venv), "MICROPIPENV_DEBUG": "1"})
+        subprocess.run(cmd, check=True, env=get_updated_env(venv))
         assert str(venv.get_version("daiquiri")) == "2.0.0"
         assert str(venv.get_version("python-json-logger")) == "0.1.11"
 
@@ -172,11 +184,11 @@ def test_install_poetry(venv):
 @pytest.mark.online
 def test_install_poetry_vcs(venv):
     """Test invoking installation using information from a Poetry project, a git version is used."""
-    cmd = [os.path.join(venv.path, "bin", "python3"), micropipenv.__file__, "install", "--method", "poetry"]
+    cmd = [os.path.join(venv.path, BIN_DIR, "python"), micropipenv.__file__, "install", "--method", "poetry"]
     venv.install(MICROPIPENV_TEST_TOML_MODULE)
     work_dir = os.path.join(_DATA_DIR, "install", "poetry_vcs")
     with cwd(work_dir):
-        subprocess.run(cmd, check=True, env={"MICROPIPENV_PIP_BIN": get_pip_path(venv), "MICROPIPENV_DEBUG": "1"})
+        subprocess.run(cmd, check=True, env=get_updated_env(venv))
         assert str(venv.get_version("daiquiri")) == "2.1.0"
         assert str(venv.get_version("python-json-logger")) == "0.1.11"
 
@@ -200,20 +212,20 @@ def test_install_pip_tools_print_lock(venv):
 @pytest.mark.online
 def test_install_pip(venv):
     """Test invoking installation when raw requirements.txt are used."""
-    cmd = [os.path.join(venv.path, "bin", "python3"), micropipenv.__file__, "install", "--method", "requirements"]
+    cmd = [os.path.join(venv.path, BIN_DIR, "python"), micropipenv.__file__, "install", "--method", "requirements"]
     work_dir = os.path.join(_DATA_DIR, "install", "requirements")
     with cwd(work_dir):
-        subprocess.run(cmd, check=True, env={"MICROPIPENV_PIP_BIN": get_pip_path(venv), "MICROPIPENV_DEBUG": "1"})
+        subprocess.run(cmd, check=True, env=get_updated_env(venv))
         assert str(venv.get_version("requests")) == "1.0.0"
 
 
 @pytest.mark.online
 def test_install_pip_vcs(venv):
     """Test installation of a package from VCS (git)."""
-    cmd = [os.path.join(venv.path, "bin", "python3"), micropipenv.__file__, "install", "--method", "requirements"]
+    cmd = [os.path.join(venv.path, BIN_DIR, "python"), micropipenv.__file__, "install", "--method", "requirements"]
     work_dir = os.path.join(_DATA_DIR, "install", "requirements_vcs")
     with cwd(work_dir):
-        subprocess.run(cmd, check=True, env={"MICROPIPENV_PIP_BIN": get_pip_path(venv), "MICROPIPENV_DEBUG": "1"})
+        subprocess.run(cmd, check=True, env=get_updated_env(venv))
         assert str(venv.get_version("daiquiri")) == "2.1.0"
         assert str(venv.get_version("python-json-logger")) is not None
 
@@ -221,19 +233,14 @@ def test_install_pip_vcs(venv):
 @pytest.mark.online
 def test_install_pip_editable(venv):
     """Test installation of an editable package which is not treated as a lock file."""
-    cmd = [os.path.join(venv.path, "bin", "python3"), micropipenv.__file__, "install", "--method", "requirements"]
+    cmd = [os.path.join(venv.path, BIN_DIR, "python"), micropipenv.__file__, "install", "--method", "requirements"]
     work_dir = os.path.join(_DATA_DIR, "install", "requirements_editable_unlocked")
     with cwd(work_dir):
         try:
-            subprocess.run(cmd, check=True, env={"MICROPIPENV_PIP_BIN": get_pip_path(venv), "MICROPIPENV_DEBUG": "1"})
+            subprocess.run(cmd, check=True, env=get_updated_env(venv))
             assert str(venv.get_version("micropipenv-editable-test")) == "3.3.3"
             assert (
-                len(
-                    glob.glob(
-                        os.path.join(venv.path, "lib", "python*", "site-packages", "micropipenv-editable-test.egg-link")
-                    )
-                )
-                == 1
+                len(glob.glob(os.path.join(venv.path, *SP_DIR, "micropipenv-editable-test.egg-link"))) == 1
             ), "No egg-link found for editable install"
             assert str(venv.get_version("q")) == "1.0"
         finally:
@@ -244,19 +251,14 @@ def test_install_pip_editable(venv):
 @pytest.mark.online
 def test_install_pip_tools_editable(venv):
     """Test installation of an editable package."""
-    cmd = [os.path.join(venv.path, "bin", "python3"), micropipenv.__file__, "install", "--method", "requirements"]
+    cmd = [os.path.join(venv.path, BIN_DIR, "python"), micropipenv.__file__, "install", "--method", "requirements"]
     work_dir = os.path.join(_DATA_DIR, "install", "requirements_editable")
     with cwd(work_dir):
         try:
-            subprocess.run(cmd, check=True, env={"MICROPIPENV_PIP_BIN": get_pip_path(venv), "MICROPIPENV_DEBUG": "1"})
+            subprocess.run(cmd, check=True, env=get_updated_env(venv))
             assert str(venv.get_version("micropipenv-editable-test")) == "3.2.1"
             assert (
-                len(
-                    glob.glob(
-                        os.path.join(venv.path, "lib", "python*", "site-packages", "micropipenv-editable-test.egg-link")
-                    )
-                )
-                == 1
+                len(glob.glob(os.path.join(venv.path, *SP_DIR, "micropipenv-editable-test.egg-link"))) == 1
             ), "No egg-link found for editable install"
             assert str(venv.get_version("daiquiri")) == "2.0.0"
             assert str(venv.get_version("python-json-logger")) == "0.1.11"
@@ -275,7 +277,7 @@ def test_install_pip_tools_direct_reference(venv):
 
     See https://pip.pypa.io/en/stable/reference/pip_install/#requirement-specifiers
     """
-    cmd = [os.path.join(venv.path, "bin", "python3"), micropipenv.__file__, "install", "--method", "requirements"]
+    cmd = [os.path.join(venv.path, BIN_DIR, "python"), micropipenv.__file__, "install", "--method", "requirements"]
     work_dir = os.path.join(_DATA_DIR, "install", "pip-tools_direct_reference")
     with cwd(work_dir):
         with open("requirements.txt", "w") as requirements_file:
@@ -286,7 +288,7 @@ def test_install_pip_tools_direct_reference(venv):
                 )
             )
 
-        subprocess.run(cmd, check=True, env={"MICROPIPENV_PIP_BIN": get_pip_path(venv), "MICROPIPENV_DEBUG": "1"})
+        subprocess.run(cmd, check=True, env=get_updated_env(venv))
         assert str(venv.get_version("micropipenv")) == "0.0.0"
 
 
@@ -827,7 +829,7 @@ def test_import_toml(venv):
     """Test the correct order of toml modules."""
     # cmd to run the function inside the venv
     cmd = [
-        os.path.join(venv.path, "bin", "python3"),
+        os.path.join(venv.path, BIN_DIR, "python"),
         "-c",
         "from micropipenv import _import_toml; print(_import_toml())",
     ]