core/ci: fix windows-specific issues

- use portable separators
- paths should be prepended with r' (so backwards slash isn't treated as escaping)
- sqlite connections should be closed (otherwise windows fails to remove the underlying db file)
- workaround for emojis via PYTHONUTF8=1 test for now
- make ZipPath portable
- properly use tox python environment everywhere

  this was causing issues on Windows
  e.g.
      WARNING: test command found but not installed in testenv
        cmd: C:\hostedtoolcache\windows\Python\3.9.12\x64\python3.EXE
This commit is contained in:
Dima Gerasimov 2022-05-02 18:26:22 +01:00 committed by karlicoss
parent 637982a5ba
commit 64a4782f0e
11 changed files with 67 additions and 37 deletions

15
demo.py
View file

@ -3,6 +3,7 @@ from subprocess import check_call, DEVNULL
from shutil import copy, copytree from shutil import copy, copytree
import os import os
from os.path import abspath from os.path import abspath
from sys import executable as python
from pathlib import Path from pathlib import Path
my_repo = Path(__file__).absolute().parent my_repo = Path(__file__).absolute().parent
@ -18,12 +19,12 @@ def run():
# 2. prepare repositories you'd be using. For this demo we only set up Hypothesis # 2. prepare repositories you'd be using. For this demo we only set up Hypothesis
tox = 'TOX' in os.environ tox = 'TOX' in os.environ
if tox: # tox doesn't like --user flag if tox: # tox doesn't like --user flag
check_call('pip3 install git+https://github.com/karlicoss/hypexport.git'.split()) check_call(f'{python} -m pip install git+https://github.com/karlicoss/hypexport.git'.split())
else: else:
try: try:
import hypexport import hypexport
except ModuleNotFoundError: except ModuleNotFoundError:
check_call('pip3 install --user git+https://github.com/karlicoss/hypexport.git'.split()) check_call(f'{python} -m pip --user git+https://github.com/karlicoss/hypexport.git'.split())
# 3. prepare some demo Hypothesis data # 3. prepare some demo Hypothesis data
@ -48,7 +49,7 @@ def run():
# 4. now we can use it! # 4. now we can use it!
os.chdir(my_repo) os.chdir(my_repo)
check_call(['python3', '-c', ''' check_call([python, '-c', '''
import my.hypothesis import my.hypothesis
pages = my.hypothesis.pages() pages = my.hypothesis.pages()
@ -106,12 +107,16 @@ def named_temp_dir(name: str):
""" """
Fixed name tmp dir Fixed name tmp dir
""" """
td = (Path('/tmp') / name) import tempfile
td = Path(tempfile.gettempdir()) / name
try: try:
td.mkdir(exist_ok=False) td.mkdir(exist_ok=False)
yield td yield td
finally: finally:
import shutil import os, shutil
skip_cleanup = 'CI' in os.environ and os.name == 'nt'
# TODO hmm for some reason cleanup on windows causes AccessError
if not skip_cleanup:
shutil.rmtree(str(td)) shutil.rmtree(str(td))

View file

@ -19,7 +19,7 @@ from my.core import Paths, PathIsh
class hypothesis: class hypothesis:
# expects outputs from https://github.com/karlicoss/hypexport # expects outputs from https://github.com/karlicoss/hypexport
# (it's just the standard Hypothes.is export format) # (it's just the standard Hypothes.is export format)
export_path: Paths = '/path/to/hypothesis/data' export_path: Paths = r'/path/to/hypothesis/data'
class instapaper: class instapaper:
export_path: Paths = '' export_path: Paths = ''

View file

@ -16,6 +16,7 @@ NOT_HPI_MODULE_VAR = '__NOT_HPI_MODULE__'
### ###
import ast import ast
import os
from typing import Optional, Sequence, List, NamedTuple, Iterable, cast, Any from typing import Optional, Sequence, List, NamedTuple, Iterable, cast, Any
from pathlib import Path from pathlib import Path
import re import re
@ -151,7 +152,7 @@ def _modules_under_root(my_root: Path) -> Iterable[HPIModule]:
mp = f.relative_to(my_root.parent) mp = f.relative_to(my_root.parent)
if mp.name == '__init__.py': if mp.name == '__init__.py':
mp = mp.parent mp = mp.parent
m = str(mp.with_suffix('')).replace('/', '.') m = str(mp.with_suffix('')).replace(os.sep, '.')
if ignored(m): if ignored(m):
continue continue
a: ast.Module = ast.parse(f.read_text()) a: ast.Module = ast.parse(f.read_text())
@ -192,7 +193,7 @@ def test() -> None:
def test_demo() -> None: def test_demo() -> None:
demo = module_by_name('my.demo') demo = module_by_name('my.demo')
assert demo.doc is not None assert demo.doc is not None
assert str(demo.file) == 'my/demo.py' assert demo.file == Path('my', 'demo.py')
assert demo.requires is None assert demo.requires is None

View file

@ -203,7 +203,9 @@ class ZipPath(ZipPathBase):
def stat(self) -> os.stat_result: def stat(self) -> os.stat_result:
# NOTE: zip datetimes have no notion of time zone, usually they just keep local time? # NOTE: zip datetimes have no notion of time zone, usually they just keep local time?
# see https://en.wikipedia.org/wiki/ZIP_(file_format)#Structure # see https://en.wikipedia.org/wiki/ZIP_(file_format)#Structure
dt = datetime(*self.root.getinfo(str(self.subpath)).date_time) # note: seems that zip always uses forward slash, regardless OS?
zip_subpath = '/'.join(self.subpath.parts)
dt = datetime(*self.root.getinfo(zip_subpath).date_time)
ts = int(dt.timestamp()) ts = int(dt.timestamp())
params = dict( params = dict(
st_mode=0, st_mode=0,

View file

@ -48,4 +48,5 @@ def sqlite_copy_and_open(db: PathIsh) -> sqlite3.Connection:
with sqlite3.connect(str(tdir / dp.name)) as conn: with sqlite3.connect(str(tdir / dp.name)) as conn:
from .compat import sqlite_backup from .compat import sqlite_backup
sqlite_backup(source=conn, dest=dest) sqlite_backup(source=conn, dest=dest)
conn.close()
return dest return dest

View file

@ -229,9 +229,9 @@ def test_bad_modules(tmp_path: Path) -> None:
(par / 'malicious.py').write_text(f''' (par / 'malicious.py').write_text(f'''
from pathlib import Path from pathlib import Path
Path('{xx}').write_text('aaand your data is gone!') Path(r'{xx}').write_text('aaand your data is gone!')
raise RuntimeError("FAIL ON IMPORT! naughy.") raise RuntimeError("FAIL ON IMPORT! naughty.")
def stats(): def stats():
return [1, 2, 3] return [1, 2, 3]

View file

@ -1,4 +1,14 @@
import os
from subprocess import check_call from subprocess import check_call
def test_lists_modules() -> None: def test_lists_modules() -> None:
check_call(['hpi', 'modules']) # hack PYTHONUTF8 for windows
# see https://github.com/karlicoss/promnesia/issues/274
# https://memex.zulipchat.com/#narrow/stream/279600-promnesia/topic/indexing.3A.20utf8.28emoji.29.20filenames.20in.20Windows
# necessary for this test cause emooji is causing trouble
# TODO need to fix it properly
env = {
**os.environ,
'PYTHONUTF8': '1',
}
check_call(['hpi', 'modules'], env=env)

View file

@ -1,9 +1,14 @@
# TODO need fdfind on CI?
from pathlib import Path from pathlib import Path
from more_itertools import bucket from more_itertools import bucket
import pytest import pytest
import os
pytestmark = pytest.mark.skipif(
os.name == 'nt',
reason='TODO figure out how to install fd-find on Windows',
)
def test() -> None: def test() -> None:
from my.coding.commits import commits from my.coding.commits import commits

View file

@ -76,24 +76,25 @@ def test_zippath() -> None:
hash(zp) hash(zp)
assert zp.exists() assert zp.exists()
assert (zp / 'gdpr_export/comments').exists() assert (zp / 'gdpr_export' / 'comments').exists()
# check str constructor just in case # check str constructor just in case
assert (ZipPath(str(target)) / 'gdpr_export/comments').exists() assert (ZipPath(str(target)) / 'gdpr_export' / 'comments').exists()
assert not (ZipPath(str(target)) / 'whatever').exists() assert not (ZipPath(str(target)) / 'whatever').exists()
matched = list(zp.rglob('*')) matched = list(zp.rglob('*'))
assert len(matched) > 0 assert len(matched) > 0
assert all(p.filepath == target for p in matched), matched assert all(p.filepath == target for p in matched), matched
rpaths = [str(p.relative_to(zp)) for p in matched] rpaths = [p.relative_to(zp) for p in matched]
gdpr_export = Path('gdpr_export')
assert rpaths == [ assert rpaths == [
'gdpr_export', gdpr_export,
'gdpr_export/comments', gdpr_export / 'comments',
'gdpr_export/comments/comments.json', gdpr_export / 'comments' / 'comments.json',
'gdpr_export/profile', gdpr_export / 'profile',
'gdpr_export/profile/settings.json', gdpr_export / 'profile' / 'settings.json',
'gdpr_export/messages', gdpr_export / 'messages',
'gdpr_export/messages/index.csv', gdpr_export / 'messages' / 'index.csv',
], rpaths ], rpaths
@ -103,14 +104,15 @@ def test_zippath() -> None:
# same for this one # same for this one
# assert ZipPath(Path('test'), 'whatever').absolute() == ZipPath(Path('test').absolute(), 'whatever') # assert ZipPath(Path('test'), 'whatever').absolute() == ZipPath(Path('test').absolute(), 'whatever')
assert (ZipPath(target) / 'gdpr_export/comments').exists() assert (ZipPath(target) / 'gdpr_export' / 'comments').exists()
jsons = [str(p.relative_to(zp / 'gdpr_export')) for p in zp.rglob('*.json')] jsons = [p.relative_to(zp / 'gdpr_export') for p in zp.rglob('*.json')]
assert jsons == [ assert jsons == [
'comments/comments.json', Path('comments','comments.json'),
'profile/settings.json', Path('profile','settings.json'),
] ]
# NOTE: hmm interesting, seems that ZipPath is happy with forward slash regardless OS?
assert list(zp.rglob('mes*')) == [ZipPath(target, 'gdpr_export/messages')] assert list(zp.rglob('mes*')) == [ZipPath(target, 'gdpr_export/messages')]
iterdir_res = list((zp / 'gdpr_export').iterdir()) iterdir_res = list((zp / 'gdpr_export').iterdir())
@ -118,7 +120,7 @@ def test_zippath() -> None:
assert all(isinstance(p, Path) for p in iterdir_res) assert all(isinstance(p, Path) for p in iterdir_res)
# date recorded in the zip archive # date recorded in the zip archive
assert (zp / 'gdpr_export/comments/comments.json').stat().st_mtime > 1625000000 assert (zp / 'gdpr_export' / 'comments' / 'comments.json').stat().st_mtime > 1625000000
# TODO ugh. # TODO ugh.
# unzip -l shows the date as 2021-07-01 09:43 # unzip -l shows the date as 2021-07-01 09:43
# however, python reads it as 2021-07-01 01:43 ?? # however, python reads it as 2021-07-01 01:43 ??

View file

@ -43,20 +43,24 @@ def _test_do_copy(db: Path) -> None:
shutil.copy(db, cdb) shutil.copy(db, cdb)
with sqlite3.connect(str(cdb)) as conn_copy: with sqlite3.connect(str(cdb)) as conn_copy:
assert len(list(conn_copy.execute('SELECT * FROM testtable'))) == 5 assert len(list(conn_copy.execute('SELECT * FROM testtable'))) == 5
conn_copy.close()
def _test_do_immutable(db: Path) -> None: def _test_do_immutable(db: Path) -> None:
# in readonly mode doesn't touch # in readonly mode doesn't touch
with sqlite_connect_immutable(db) as conn_imm: with sqlite_connect_immutable(db) as conn_imm:
assert len(list(conn_imm.execute('SELECT * FROM testtable'))) == 5 assert len(list(conn_imm.execute('SELECT * FROM testtable'))) == 5
conn_imm.close()
def _test_do_copy_and_open(db: Path) -> None: def _test_do_copy_and_open(db: Path) -> None:
with sqlite_copy_and_open(db) as conn_mem: with sqlite_copy_and_open(db) as conn_mem:
assert len(list(conn_mem.execute('SELECT * FROM testtable'))) == 10 assert len(list(conn_mem.execute('SELECT * FROM testtable'))) == 10
conn_mem.close()
def _test_open_asis(db: Path) -> None: def _test_open_asis(db: Path) -> None:
# NOTE: this also works... but leaves some potential for DB corruption # NOTE: this also works... but leaves some potential for DB corruption
with sqlite3.connect(str(db)) as conn_db_2: with sqlite3.connect(str(db)) as conn_db_2:
assert len(list(conn_db_2.execute('SELECT * FROM testtable'))) == 10 assert len(list(conn_db_2.execute('SELECT * FROM testtable'))) == 10
conn_db_2.close()

16
tox.ini
View file

@ -12,7 +12,7 @@ passenv = CI CI_*
[testenv:tests-core] [testenv:tests-core]
commands = commands =
pip install -e .[testing] pip install -e .[testing]
python3 -m pytest \ {envpython} -m pytest \
tests/core.py \ tests/core.py \
tests/sqlite.py \ tests/sqlite.py \
tests/get_files.py \ tests/get_files.py \
@ -29,7 +29,7 @@ commands =
# installed to test my.core.serialize while using simplejson and not orjson # installed to test my.core.serialize while using simplejson and not orjson
pip install simplejson pip install simplejson
python3 -m pytest \ {envpython} -m pytest \
tests/serialize_simplejson.py \ tests/serialize_simplejson.py \
{posargs} {posargs}
@ -52,28 +52,28 @@ commands =
hpi module install my.reddit.rexport hpi module install my.reddit.rexport
python3 -m pytest tests \ {envpython} -m pytest tests \
# ignore some tests which might take a while to run on ci.. # ignore some tests which might take a while to run on ci..
--ignore tests/takeout.py \ --ignore tests/takeout.py \
--ignore tests/extra/polar.py \ --ignore tests/extra/polar.py \
# dont run simplejson compatibility test since orjson is now installed # dont run simplejson compatibility test since orjson is now installed
--ignore tests/serialize_simplejson.py --ignore tests/serialize_simplejson.py \
{posargs} {posargs}
[testenv:demo] [testenv:demo]
commands = commands =
pip install git+https://github.com/karlicoss/hypexport pip install git+https://github.com/karlicoss/hypexport
./demo.py {envpython} ./demo.py
[testenv:mypy-core] [testenv:mypy-core]
whitelist_externals = cat allowlist_externals = cat
commands = commands =
pip install -e .[testing,optional] pip install -e .[testing,optional]
pip install orgparse # used it core.orgmode? pip install orgparse # used it core.orgmode?
# todo add tests? # todo add tests?
python3 -m mypy --install-types --non-interactive \ {envpython} -m mypy --install-types --non-interactive \
-p my.core \ -p my.core \
--txt-report .coverage.mypy-core \ --txt-report .coverage.mypy-core \
--html-report .coverage.mypy-core \ --html-report .coverage.mypy-core \
@ -109,7 +109,7 @@ commands =
# todo fuck. -p my.github isn't checking the subpackages?? wtf... # todo fuck. -p my.github isn't checking the subpackages?? wtf...
# guess it wants .pyi file?? # guess it wants .pyi file??
python3 -m mypy --install-types --non-interactive \ {envpython} -m mypy --install-types --non-interactive \
-p my.browser \ -p my.browser \
-p my.endomondo \ -p my.endomondo \
-p my.github.ghexport \ -p my.github.ghexport \