Repository: hylang/hy

Issue:
Get tests passing on Windows
GitHub Actions supports Windows, so we might as well bite this bullet. I'm currently working on this at https://github.com/Kodiologist/hy/tree/winders.


Discussion:

Resolving patch:
diff --git a/hy/cmdline.py b/hy/cmdline.py
--- a/hy/cmdline.py
+++ b/hy/cmdline.py
@@ -758,7 +758,7 @@ def hy2py_main():
         hst = hy_parse(source, filename=filename)
 
     if options.with_source:
-        _print_for_windows(hst)
+        print(hst)
         print()
         print()
 
@@ -766,29 +766,16 @@ def hy2py_main():
         _ast = hy_compile(hst, '__main__', filename=filename, source=source)
 
     if options.with_ast:
-        _print_for_windows(ast.dump(_ast))
+        print(ast.dump(_ast))
         print()
         print()
 
     if not options.without_python:
-        _print_for_windows(ast.unparse(_ast))
+        print(ast.unparse(_ast))
 
     parser.exit(0)
 
 
-# need special printing on Windows in case the
-# codepage doesn't support utf-8 characters
-def _print_for_windows(src):
-    import platform
-    if platform.system() == "Windows":
-        for line in src.split("\n"):
-            try:
-                print(line)
-            except:
-                print(line.encode('utf-8'))
-    else:
-        print(src)
-
 # remove PYTHON* environment variables,
 # such as "PYTHONPATH"
 def _remove_python_envs():
diff --git a/hy/macros.py b/hy/macros.py
--- a/hy/macros.py
+++ b/hy/macros.py
@@ -1,4 +1,5 @@
 import sys
+import os
 import builtins
 import importlib
 import inspect
@@ -107,7 +108,7 @@ def _get_filename(module):
     target_filename = _get_filename(target_module)
 
     return (source_filename and target_filename and
-            source_filename == target_filename)
+            os.path.samefile(source_filename, target_filename))
 
 
 def require(source_module, target_module, assignments, prefix=""):


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -8,10 +8,18 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
+        name-prefix: ['']
+        os: [ubuntu-latest]
         python: [3.6, 3.7, 3.8, 3.9, 3.10-dev, pypy-3.7]
+        include:
+          # To keep the overall number of runs low, we test Windows
+          # only on the latest CPython.
+          - name-prefix: 'win-'
+            os: windows-latest
+            python: 3.10-dev
 
-    name: ${{ matrix.python }}
-    runs-on: ubuntu-latest
+    name: ${{ format('{0}{1}', matrix.name-prefix, matrix.python) }}
+    runs-on: ${{ matrix.os }}
     env:
       TERM: xterm-256color
         # This is needed to avoid a terminfo-related crash when
diff --git a/tests/importer/test_importer.py b/tests/importer/test_importer.py
--- a/tests/importer/test_importer.py
+++ b/tests/importer/test_importer.py
@@ -1,10 +1,9 @@
-import os
 import sys
 import ast
-import tempfile
 import runpy
 import importlib
 
+from pathlib import Path
 from fractions import Fraction
 from importlib import reload
 
@@ -21,11 +20,9 @@
 def test_basics():
     "Make sure the basics of the importer work"
 
-    assert os.path.isfile('tests/resources/__init__.py')
     resources_mod = importlib.import_module('tests.resources')
     assert hasattr(resources_mod, 'kwtest')
 
-    assert os.path.isfile('tests/resources/bin/__init__.hy')
     bin_mod = importlib.import_module('tests.resources.bin')
     assert hasattr(bin_mod, '_null_fn_for_import_test')
 
@@ -55,8 +52,9 @@ def test_stringer():
 
 
 def test_imports():
-    path = os.getcwd() + "/tests/resources/importer/a.hy"
-    testLoader = HyLoader("tests.resources.importer.a", path)
+    testLoader = HyLoader(
+        "tests.resources.importer.a",
+        "tests/resources/importer/a.hy")
     spec = importlib.util.spec_from_loader(testLoader.name, testLoader)
     mod = importlib.util.module_from_spec(spec)
 
@@ -84,29 +82,26 @@ def test_import_error_cleanup():
 
 @pytest.mark.skipif(sys.dont_write_bytecode,
                     reason="Bytecode generation is suppressed")
-def test_import_autocompiles():
+def test_import_autocompiles(tmp_path):
     "Test that (import) byte-compiles the module."
 
-    with tempfile.NamedTemporaryFile(suffix='.hy', delete=True) as f:
-        f.write(b'(defn pyctest [s] (+ "X" s "Y"))')
-        f.flush()
+    p = tmp_path / 'mymodule.hy'
+    p.write_text('(defn pyctest [s] (+ "X" s "Y"))')
 
-        pyc_path = importlib.util.cache_from_source(f.name)
-
-        try:
-            os.remove(pyc_path)
-        except OSError:
-            pass
-
-        spec = importlib.util.spec_from_file_location('mymodule', f.name)
+    def import_from_path(path):
+        spec = importlib.util.spec_from_file_location('mymodule', path)
         module = importlib.util.module_from_spec(spec)
         spec.loader.exec_module(module)
+        return module
 
-        assert hasattr(module, 'pyctest')
-        assert os.path.exists(pyc_path)
-
-        os.remove(pyc_path)
+    assert import_from_path(p).pyctest('flim') == 'XflimY'
+    assert Path(importlib.util.cache_from_source(p)).exists()
 
+    # Try running the bytecode.
+    assert (
+        import_from_path(importlib.util.cache_from_source(p))
+           .pyctest('flam')
+        == 'XflamY')
 
 def test_eval():
     def eval_str(s):
@@ -125,7 +120,7 @@ def eval_str(s):
         pow(num, 2) for num in range(100) if num % 2 == 1]
 
 
-def test_reload():
+def test_reload(tmp_path, monkeypatch):
     """Generate a test module, confirm that it imports properly (and puts the
     module in `sys.modules`), then modify the module so that it produces an
     error when reloaded.  Next, fix the error, reload, and check that the
@@ -135,18 +130,16 @@ def test_reload():
     """
 
     def unlink(filename):
-        os.unlink(source)
+        Path(source).unlink()
         bytecode = importlib.util.cache_from_source(source)
-        if os.path.isfile(bytecode):
-            os.unlink(bytecode)
+        if Path(bytecode).is_file():
+            Path(bytecode).unlink()
 
     TESTFN = 'testfn'
-    source = TESTFN + os.extsep + "hy"
-    with open(source, "w") as f:
-        f.write("(setv a 1)")
-        f.write("(setv b 2)")
+    source = tmp_path / (TESTFN + ".hy")
+    source.write_text("(setv a 1)  (setv b 2)")
 
-    sys.path.insert(0, os.curdir)
+    monkeypatch.syspath_prepend(tmp_path)
     try:
         mod = importlib.import_module(TESTFN)
         assert TESTFN in sys.modules
@@ -160,9 +153,7 @@ def unlink(filename):
         unlink(source)
 
         # Now damage the module.
-        with open(source, "w") as f:
-            f.write("(setv a 10)")
-            f.write("(setv b (// 20 0))")
+        source.write_text("(setv a 10)  (setv b (// 20 0))")
 
         with pytest.raises(ZeroDivisionError):
             reload(mod)
@@ -179,9 +170,7 @@ def unlink(filename):
         # Now fix the issue and reload the module.
         unlink(source)
 
-        with open(source, "w") as f:
-            f.write("(setv a 11)")
-            f.write("(setv b (// 20 1))")
+        source.write_text("(setv a 11)  (setv b (// 20 1))")
 
         reload(mod)
 
@@ -191,13 +180,10 @@ def unlink(filename):
         assert mod.a == 11
         assert mod.b == 20
 
-        # Now cause a syntax error
+        # Now cause a syntax error (a missing parenthesis)
         unlink(source)
 
-        with open(source, "w") as f:
-            # Missing paren...
-            f.write("(setv a 11")
-            f.write("(setv b (// 20 1))")
+        source.write_text("(setv a 11  (setv b (// 20 1))")
 
         with pytest.raises(PrematureEndOfInput):
             reload(mod)
@@ -211,9 +197,7 @@ def unlink(filename):
         # Fix it and retry
         unlink(source)
 
-        with open(source, "w") as f:
-            f.write("(setv a 12)")
-            f.write("(setv b (// 10 1))")
+        source.write_text("(setv a 12)  (setv b (// 10 1))")
 
         reload(mod)
 
@@ -224,10 +208,8 @@ def unlink(filename):
         assert mod.b == 10
 
     finally:
-        del sys.path[0]
         if TESTFN in sys.modules:
             del sys.modules[TESTFN]
-        unlink(source)
 
 
 def test_reload_reexecute(capsys):
@@ -242,49 +224,34 @@ def test_reload_reexecute(capsys):
     assert capsys.readouterr().out == 'hello world\n'
 
 
-def test_circular():
+def test_circular(monkeypatch):
     """Test circular imports by creating a temporary file/module that calls a
     function that imports itself."""
-    sys.path.insert(0, os.path.abspath('tests/resources/importer'))
-    try:
-        mod = runpy.run_module('circular')
-        assert mod['f']() == 1
-    finally:
-        sys.path.pop(0)
+    monkeypatch.syspath_prepend('tests/resources/importer')
+    assert runpy.run_module('circular')['f']() == 1
 
 
-def test_shadowed_basename():
+def test_shadowed_basename(monkeypatch):
     """Make sure Hy loads `.hy` files instead of their `.py` counterparts (.e.g
     `__init__.py` and `__init__.hy`).
     """
-    sys.path.insert(0, os.path.realpath('tests/resources/importer'))
-    try:
-        assert os.path.isfile('tests/resources/importer/foo/__init__.hy')
-        assert os.path.isfile('tests/resources/importer/foo/__init__.py')
-        assert os.path.isfile('tests/resources/importer/foo/some_mod.hy')
-        assert os.path.isfile('tests/resources/importer/foo/some_mod.py')
-
-        foo = importlib.import_module('foo')
-        assert foo.__file__.endswith('foo/__init__.hy')
-        assert foo.ext == 'hy'
-        some_mod = importlib.import_module('foo.some_mod')
-        assert some_mod.__file__.endswith('foo/some_mod.hy')
-        assert some_mod.ext == 'hy'
-    finally:
-        sys.path.pop(0)
+    monkeypatch.syspath_prepend('tests/resources/importer')
+    foo = importlib.import_module('foo')
+    assert Path(foo.__file__).name == '__init__.hy'
+    assert foo.ext == 'hy'
+    some_mod = importlib.import_module('foo.some_mod')
+    assert Path(some_mod.__file__).name == 'some_mod.hy'
+    assert some_mod.ext == 'hy'
 
 
-def test_docstring():
+def test_docstring(monkeypatch):
     """Make sure a module's docstring is loaded."""
-    sys.path.insert(0, os.path.realpath('tests/resources/importer'))
-    try:
-        mod = importlib.import_module('docstring')
-        expected_doc = ("This module has a docstring.\n\n"
-                        "It covers multiple lines, too!\n")
-        assert mod.__doc__ == expected_doc
-        assert mod.a == 1
-    finally:
-        sys.path.pop(0)
+    monkeypatch.syspath_prepend('tests/resources/importer')
+    mod = importlib.import_module('docstring')
+    expected_doc = ("This module has a docstring.\n\n"
+                    "It covers multiple lines, too!\n")
+    assert mod.__doc__ == expected_doc
+    assert mod.a == 1
 
 
 def test_hy_python_require():
@@ -294,8 +261,9 @@ def test_hy_python_require():
 
 
 def test_filtered_importlib_frames(capsys):
-    path = os.getcwd() + "/tests/resources/importer/compiler_error.hy"
-    testLoader = HyLoader("tests.resources.importer.compiler_error", path)
+    testLoader = HyLoader(
+       "tests.resources.importer.compiler_error",
+       "tests/resources/importer/compiler_error.hy")
     spec = importlib.util.spec_from_loader(testLoader.name, testLoader)
     mod = importlib.util.module_from_spec(spec)
 
diff --git a/tests/importer/test_pyc.py b/tests/importer/test_pyc.py
deleted file mode 100644
--- a/tests/importer/test_pyc.py
+++ /dev/null
@@ -1,23 +0,0 @@
-import os
-import importlib.util
-import py_compile
-import tempfile
-
-import hy.importer
-
-
-def test_pyc():
-    """Test pyc compilation."""
-    with tempfile.NamedTemporaryFile(suffix='.hy') as f:
-        f.write(b'(defn pyctest [s] (+ "X" s "Y"))')
-        f.flush()
-
-        cfile = py_compile.compile(f.name)
-        assert os.path.exists(cfile)
-
-        try:
-            mod = hy.importer._import_from_path('pyc', cfile)
-        finally:
-            os.remove(cfile)
-
-        assert mod.pyctest('Foo') == 'XFooY'
diff --git a/tests/resources/bin/circular_macro_require.hy b/tests/resources/bin/circular_macro_require.hy
--- a/tests/resources/bin/circular_macro_require.hy
+++ b/tests/resources/bin/circular_macro_require.hy
@@ -1,8 +1,8 @@
 (defmacro bar [expr]
-  `(print ~expr))
+  `(print (.upper ~expr)))
 
 (defmacro foo [expr]
   `(do (require tests.resources.bin.circular-macro-require [bar])
        (bar ~expr)))
 
-(foo 42)
+(foo "wowie")
diff --git a/tests/test_bin.py b/tests/test_bin.py
--- a/tests/test_bin.py
+++ b/tests/test_bin.py
@@ -297,10 +297,6 @@ def test_bin_hy_icmd_file():
     output, _ = run_cmd("hy -i resources/icmd_test_file.hy", "(ideas)")
     assert "Hy!" in output
 
-    file_relative_path = os.path.realpath(os.path.split('tests/resources/relative_import.hy')[0])
-
-    output, _ = run_cmd("hy -i tests/resources/relative_import.hy None")
-    assert file_relative_path in output
 
 def test_bin_hy_icmd_and_spy():
     output, _ = run_cmd("hy --spy -i \"(+ [] [])\"", "(+ 1 1)")
@@ -424,7 +420,7 @@ def test_bin_hy_file_sys_path():
     file_relative_path = os.path.realpath(file_path)
 
     output, _ = run_cmd("hy tests/resources/relative_import.hy")
-    assert file_relative_path in output
+    assert repr(file_relative_path) in output
 
 def test_bin_hyc_file_sys_path():
     # similar to test_bin_hy_file_sys_path, test hyc and hy2py to make sure
@@ -440,7 +436,7 @@ def test_bin_hyc_file_sys_path():
         assert not os.path.exists(cache_from_source(file_relative_path))
 
         output, _ = run_cmd(f"{binary} {test_file}")
-        assert file_relative_path in output
+        assert repr(file_relative_path) in output
 
 
 def test_bin_hy_module_no_main():
@@ -450,7 +446,7 @@ def test_bin_hy_module_no_main():
 
 def test_bin_hy_sys_executable():
     output, _ = run_cmd("hy -c '(do (import sys) (print sys.executable))'")
-    assert output.strip().endswith('/hy')
+    assert os.path.basename(output.strip()) == 'hy'
 
 
 def test_bin_hy_file_no_extension():
@@ -468,12 +464,12 @@ def test_bin_hy_circular_macro_require():
     rm(cache_from_source(test_file))
     assert not os.path.exists(cache_from_source(test_file))
     output, _ = run_cmd("hy {}".format(test_file))
-    assert "42" == output.strip()
+    assert output.strip() == "WOWIE"
 
     # Now, with bytecode
     assert os.path.exists(cache_from_source(test_file))
     output, _ = run_cmd("hy {}".format(test_file))
-    assert "42" == output.strip()
+    assert output.strip() == "WOWIE"
 
 def test_bin_hy_macro_require():
     """Confirm that a `require` will load macros into the non-module namespace
@@ -486,12 +482,12 @@ def test_bin_hy_macro_require():
     rm(cache_from_source(test_file))
     assert not os.path.exists(cache_from_source(test_file))
     output, _ = run_cmd("hy {}".format(test_file))
-    assert "abc" == output.strip()
+    assert output.strip() == "abc"
 
     # Now, with bytecode
     assert os.path.exists(cache_from_source(test_file))
     output, _ = run_cmd("hy {}".format(test_file))
-    assert "abc" == output.strip()
+    assert output.strip() == "abc"
 
 
 def test_bin_hy_tracebacks():
diff --git a/tests/test_completer.py b/tests/test_completer.py
--- a/tests/test_completer.py
+++ b/tests/test_completer.py
@@ -2,25 +2,22 @@
 import sys
 from hy.completer import completion
 import pytest
-import tempfile
 
 
 @pytest.mark.skipif(
     "readline" not in sys.modules,
     reason="Module 'readline' is not available.")
-def test_history_custom_location():
+def test_history_custom_location(tmp_path):
     import readline
 
     expected_entry = '(print "Hy, custom history file!")'
 
-    with tempfile.TemporaryDirectory() as tmp:
-        history_location = tmp + os.sep + ".hy-custom-history"
-        os.environ["HY_HISTORY"] = history_location
+    history_location = tmp_path / ".hy-custom-history"
+    os.environ["HY_HISTORY"] = str(history_location)
 
-        with completion():
-            readline.clear_history()
-            readline.add_history(expected_entry)
+    with completion():
+        readline.clear_history()
+        readline.add_history(expected_entry)
 
-        with open(history_location, "r") as hf:
-            actual_entry = hf.readline()
-            assert expected_entry in actual_entry
+    actual_entry = history_location.read_text()
+    assert expected_entry in actual_entry
diff --git a/tests/test_hy2py.py b/tests/test_hy2py.py
--- a/tests/test_hy2py.py
+++ b/tests/test_hy2py.py
@@ -1,4 +1,4 @@
-import math, itertools, asyncio
+import os, math, itertools, asyncio
 from hy import mangle
 import hy.importer
 
@@ -10,10 +10,12 @@ def test_direct_import():
 
 def test_hy2py_import(tmpdir):
     import subprocess
-    python_code = subprocess.check_output(
-        ["hy2py", "tests/resources/pydemo.hy"]).decode("UTF-8")
     path = tmpdir.join("pydemo.py")
-    path.write(python_code)
+    with open(path, "wb") as o:
+        subprocess.check_call(
+            ["hy2py", "tests/resources/pydemo.hy"],
+            stdout = o,
+            env = {**os.environ, 'PYTHONIOENCODING': 'UTF-8'})
     assert_stuff(hy.importer._import_from_path("pydemo", path))