Skip to content

Commit 6226d05

Browse files
authored
enable appveyor testing of cwltest (#72)
* enable appveyor testing of cwltest * decrease default timeout * include mock-cwl-runner in appveyor * always upload the test results * enable pip cache preservation * make tests windows compatible * add system python to path * add test for path normalization * Fix the bug proven by the new test * speed up appveyor builds by removing pip, virtualenv installs & drop py-32bit versions
1 parent 6bffd59 commit 6226d05

File tree

6 files changed

+173
-74
lines changed

6 files changed

+173
-74
lines changed

appveyor.yml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
version: .{build}-{branch}
2+
3+
cache:
4+
- '%LOCALAPPDATA%\pip\Cache'
5+
6+
environment:
7+
SYSTEMROOT: "C:\\WINDOWS"
8+
9+
matrix:
10+
- PYTHON: "C:\\Python27-x64"
11+
PYTHON_VERSION: "2.7.x"
12+
PYTHON_ARCH: "64"
13+
14+
- PYTHON: "C:\\Python34-x64"
15+
PYTHON_VERSION: "3.4.x"
16+
PYTHON_ARCH: "64"
17+
18+
- PYTHON: "C:\\Python35-x64"
19+
PYTHON_VERSION: "3.5.x"
20+
PYTHON_ARCH: "64"
21+
22+
- PYTHON: "C:\\Python36-x64"
23+
PYTHON_VERSION: "3.6.x"
24+
PYTHON_ARCH: "64"
25+
26+
install:
27+
- "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%"
28+
29+
build_script:
30+
- |
31+
%PYTHON%\\python.exe -m pip install -U wheel pytest pytest-xdist
32+
%PYTHON%\\python.exe -m pip install -e .
33+
34+
test_script:
35+
- |
36+
%PYTHON%\\python.exe setup.py test --addopts "--verbose -p no:cacheprovider --junit-xml=tests.xml -n2"
37+
38+
on_finish:
39+
- ps: |
40+
$wc = New-Object 'System.Net.WebClient'
41+
$wc.UploadFile("https://ci.appveyor.com/api/testresults/junit/$($Env:APPVEYOR_JOB_ID)", (Resolve-Path .\tests.xml))
42+
43+
branches:
44+
only:
45+
- master

cwltest/__init__.py

Lines changed: 87 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -7,77 +7,87 @@
77
import json
88
import logging
99
import os
10-
import pipes
1110
import shutil
1211
import sys
1312
import tempfile
1413
import threading
1514
import time
15+
from typing import Any, Dict, List, Optional, Text
16+
from concurrent.futures import ThreadPoolExecutor
1617

1718
import ruamel.yaml as yaml
1819
import ruamel.yaml.scanner as yamlscanner
1920
import schema_salad.ref_resolver
20-
from concurrent.futures import ThreadPoolExecutor
2121
from six.moves import range
2222
from six.moves import zip
23-
from typing import Any, Dict, List
2423
import pkg_resources # part of setuptools
2524

2625
import junit_xml
27-
from cwltest.utils import compare, CompareFail, TestResult, REQUIRED, get_test_number_by_key
26+
from cwltest.utils import (compare, CompareFail, TestResult, REQUIRED,
27+
get_test_number_by_key)
2828

2929
_logger = logging.getLogger("cwltest")
3030
_logger.addHandler(logging.StreamHandler())
3131
_logger.setLevel(logging.INFO)
3232

3333
UNSUPPORTED_FEATURE = 33
34-
DEFAULT_TIMEOUT = 900 # 15 minutes
34+
DEFAULT_TIMEOUT = 600 # 10 minutes
3535

3636
if sys.version_info < (3, 0):
3737
import subprocess32 as subprocess
38+
from pipes import quote
3839
else:
3940
import subprocess
41+
from shlex import quote
4042

4143
templock = threading.Lock()
4244

4345

44-
def prepare_test_command(args, i, tests):
45-
# type: (argparse.Namespace, int, List[Dict[str, str]]) -> List[str]
46-
t = tests[i]
47-
test_command = [args.tool]
48-
test_command.extend(args.args)
46+
def prepare_test_command(tool, # type: str
47+
args, # type: List[str]
48+
testargs, # type: Optional[List[str]]
49+
test # type: Dict[str, str]
50+
): # type: (...) -> List[str]
51+
""" Turn the test into a command line. """
52+
test_command = [tool]
53+
test_command.extend(args)
4954

5055
# Add additional arguments given in test case
51-
if args.testargs is not None:
52-
for testarg in args.testargs:
56+
if testargs is not None:
57+
for testarg in testargs:
5358
(test_case_name, prefix) = testarg.split('==')
54-
if test_case_name in t:
55-
test_command.extend([prefix, t[test_case_name]])
59+
if test_case_name in test:
60+
test_command.extend([prefix, test[test_case_name]])
5661

5762
# Add prefixes if running on MacOSX so that boot2docker writes to /Users
5863
with templock:
59-
if 'darwin' in sys.platform and args.tool == 'cwltool':
64+
if 'darwin' in sys.platform and tool == 'cwltool':
6065
outdir = tempfile.mkdtemp(prefix=os.path.abspath(os.path.curdir))
61-
test_command.extend(["--tmp-outdir-prefix={}".format(outdir), "--tmpdir-prefix={}".format(outdir)])
66+
test_command.extend(["--tmp-outdir-prefix={}".format(outdir),
67+
"--tmpdir-prefix={}".format(outdir)])
6268
else:
6369
outdir = tempfile.mkdtemp()
6470
test_command.extend(["--outdir={}".format(outdir),
6571
"--quiet",
66-
t["tool"]])
67-
if t.get("job"):
68-
test_command.append(t["job"])
72+
os.path.normcase(test["tool"])])
73+
if test.get("job"):
74+
test_command.append(os.path.normcase(test["job"]))
6975
return test_command
7076

7177

72-
def run_test(args, i, tests, timeout):
73-
# type: (argparse.Namespace, int, List[Dict[str, str]], int) -> TestResult
78+
def run_test(args, # type: argparse.Namespace
79+
test, # type: Dict[str, str]
80+
test_number, # type: int
81+
total_tests, # type: int
82+
timeout # type: int
83+
): # type: (...) -> TestResult
7484

7585
global templock
7686

7787
out = {} # type: Dict[str,Any]
78-
outdir = outstr = outerr = test_command = None
88+
outdir = outstr = outerr = None
89+
test_command = [] # type: List[str]
7990
duration = 0.0
80-
t = tests[i]
8191
prefix = ""
8292
suffix = ""
8393
if sys.stderr.isatty():
@@ -86,12 +96,18 @@ def run_test(args, i, tests, timeout):
8696
suffix = "\n"
8797
try:
8898
process = None # type: subprocess.Popen
89-
test_command = prepare_test_command(args, i, tests)
90-
91-
if t.get("short_name"):
92-
sys.stderr.write("%sTest [%i/%i] %s: %s%s\n" % (prefix, i + 1, len(tests), t.get("short_name"), t.get("doc"), suffix))
99+
test_command = prepare_test_command(
100+
args.tool, args.args, args.testargs, test)
101+
102+
if test.get("short_name"):
103+
sys.stderr.write(
104+
"%sTest [%i/%i] %s: %s%s\n"
105+
% (prefix, test_number, total_tests, test.get("short_name"),
106+
test.get("doc"), suffix))
93107
else:
94-
sys.stderr.write("%sTest [%i/%i] %s%s\n" % (prefix, i + 1, len(tests), t.get("doc"), suffix))
108+
sys.stderr.write(
109+
"%sTest [%i/%i] %s%s\n"
110+
% (prefix, test_number, total_tests, test.get("doc"), suffix))
95111
sys.stderr.flush()
96112

97113
start_time = time.time()
@@ -104,38 +120,40 @@ def run_test(args, i, tests, timeout):
104120
raise subprocess.CalledProcessError(return_code, " ".join(test_command))
105121

106122
out = json.loads(outstr)
107-
except ValueError as v:
108-
_logger.error(str(v))
123+
except ValueError as err:
124+
_logger.error(str(err))
109125
_logger.error(outstr)
110126
_logger.error(outerr)
111127
except subprocess.CalledProcessError as err:
112128
if err.returncode == UNSUPPORTED_FEATURE:
113129
return TestResult(UNSUPPORTED_FEATURE, outstr, outerr, duration, args.classname)
114-
elif t.get("should_fail", False):
130+
if test.get("should_fail", False):
115131
return TestResult(0, outstr, outerr, duration, args.classname)
116-
else:
117-
_logger.error(u"""Test failed: %s""", " ".join([pipes.quote(tc) for tc in test_command]))
118-
_logger.error(t.get("doc"))
119-
_logger.error("Returned non-zero")
120-
_logger.error(outerr)
121-
return TestResult(1, outstr, outerr, duration, args.classname, str(err))
122-
except (yamlscanner.ScannerError, TypeError) as e:
123-
_logger.error(u"""Test failed: %s""", " ".join([pipes.quote(tc) for tc in test_command]))
132+
_logger.error(u"""Test failed: %s""", " ".join([quote(tc) for tc in test_command]))
133+
_logger.error(test.get("doc"))
134+
_logger.error(u"Returned non-zero")
135+
_logger.error(outerr)
136+
return TestResult(1, outstr, outerr, duration, args.classname, str(err))
137+
except (yamlscanner.ScannerError, TypeError) as err:
138+
_logger.error(u"""Test failed: %s""",
139+
u" ".join([quote(tc) for tc in test_command]))
124140
_logger.error(outstr)
125-
_logger.error(u"Parse error %s", str(e))
141+
_logger.error(u"Parse error %s", str(err))
126142
_logger.error(outerr)
127143
except KeyboardInterrupt:
128-
_logger.error(u"""Test interrupted: %s""", " ".join([pipes.quote(tc) for tc in test_command]))
144+
_logger.error(u"""Test interrupted: %s""",
145+
u" ".join([quote(tc) for tc in test_command]))
129146
raise
130147
except subprocess.TimeoutExpired:
131-
_logger.error(u"""Test timed out: %s""", " ".join([pipes.quote(tc) for tc in test_command]))
132-
_logger.error(t.get("doc"))
148+
_logger.error(u"""Test timed out: %s""",
149+
u" ".join([quote(tc) for tc in test_command]))
150+
_logger.error(test.get("doc"))
133151
return TestResult(2, outstr, outerr, timeout, args.classname, "Test timed out")
134152
finally:
135153
if process is not None and process.returncode is None:
136154
_logger.error(u"""Terminating lingering process""")
137155
process.terminate()
138-
for a in range(0, 3):
156+
for _ in range(0, 3):
139157
time.sleep(1)
140158
if process.poll() is not None:
141159
break
@@ -144,24 +162,25 @@ def run_test(args, i, tests, timeout):
144162

145163
fail_message = ''
146164

147-
if t.get("should_fail", False):
148-
_logger.warning(u"""Test failed: %s""", " ".join([pipes.quote(tc) for tc in test_command]))
149-
_logger.warning(t.get("doc"))
165+
if test.get("should_fail", False):
166+
_logger.warning(u"""Test failed: %s""", u" ".join([quote(tc) for tc in test_command]))
167+
_logger.warning(test.get("doc"))
150168
_logger.warning(u"Returned zero but it should be non-zero")
151169
return TestResult(1, outstr, outerr, duration, args.classname)
152170

153171
try:
154-
compare(t.get("output"), out)
172+
compare(test.get("output"), out)
155173
except CompareFail as ex:
156-
_logger.warning(u"""Test failed: %s""", " ".join([pipes.quote(tc) for tc in test_command]))
157-
_logger.warning(t.get("doc"))
174+
_logger.warning(u"""Test failed: %s""", u" ".join([quote(tc) for tc in test_command]))
175+
_logger.warning(test.get("doc"))
158176
_logger.warning(u"Compare failure %s", ex)
159177
fail_message = str(ex)
160178

161179
if outdir:
162180
shutil.rmtree(outdir, True)
163181

164-
return TestResult((1 if fail_message else 0), outstr, outerr, duration, args.classname, fail_message)
182+
return TestResult((1 if fail_message else 0), outstr, outerr, duration,
183+
args.classname, fail_message)
165184

166185

167186
def arg_parser(): # type: () -> argparse.ArgumentParser
@@ -175,17 +194,18 @@ def arg_parser(): # type: () -> argparse.ArgumentParser
175194
help="CWL runner executable to use (default 'cwl-runner'")
176195
parser.add_argument("--only-tools", action="store_true", help="Only test CommandLineTools")
177196
parser.add_argument("--junit-xml", type=str, default=None, help="Path to JUnit xml file")
178-
parser.add_argument("--test-arg", type=str, help="Additional argument given in test cases and "
179-
"required prefix for tool runner.",
180-
metavar="cache==--cache-dir", action="append", dest="testargs")
197+
parser.add_argument("--test-arg", type=str, help="Additional argument "
198+
"given in test cases and required prefix for tool runner.",
199+
default=None, metavar="cache==--cache-dir", action="append", dest="testargs")
181200
parser.add_argument("args", help="arguments to pass first to tool runner", nargs=argparse.REMAINDER)
182201
parser.add_argument("-j", type=int, default=1, help="Specifies the number of tests to run simultaneously "
183202
"(defaults to one).")
184203
parser.add_argument("--verbose", action="store_true", help="More verbose output during test run.")
185204
parser.add_argument("--classname", type=str, default="", help="Specify classname for the Test Suite.")
186-
parser.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT, help="Time of execution in seconds after "
187-
"which the test will be skipped. "
188-
"Defaults to 900 sec (15 minutes)")
205+
parser.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT,
206+
help="Time of execution in seconds after which the test will be "
207+
"skipped. Defaults to {} seconds ({} minutes).".format(
208+
DEFAULT_TIMEOUT, DEFAULT_TIMEOUT/60))
189209

190210
pkg = pkg_resources.require("cwltest")
191211
if pkg:
@@ -256,14 +276,14 @@ def main(): # type: () -> int
256276
if test_number:
257277
ntest.append(test_number)
258278
else:
259-
_logger.error('Test with short name "%s" not found ' % s)
279+
_logger.error('Test with short name "%s" not found ', s)
260280
return 1
261281
else:
262282
ntest = list(range(0, len(tests)))
263283

264284
total = 0
265285
with ThreadPoolExecutor(max_workers=args.j) as executor:
266-
jobs = [executor.submit(run_test, args, i, tests, args.timeout)
286+
jobs = [executor.submit(run_test, args, tests[i], i+1, len(tests), args.timeout)
267287
for i in ntest]
268288
try:
269289
for i, job in zip(ntest, jobs):
@@ -294,18 +314,19 @@ def main(): # type: () -> int
294314
_logger.error("Tests interrupted")
295315

296316
if args.junit_xml:
297-
with open(args.junit_xml, 'w') as fp:
298-
junit_xml.TestSuite.to_file(fp, [report])
317+
with open(args.junit_xml, 'w') as xml:
318+
junit_xml.TestSuite.to_file(xml, [report])
299319

300320
if failures == 0 and unsupported == 0:
301321
_logger.info("All tests passed")
302322
return 0
303-
elif failures == 0 and unsupported > 0:
304-
_logger.warning("%i tests passed, %i unsupported features", total - unsupported, unsupported)
323+
if failures == 0 and unsupported > 0:
324+
_logger.warning("%i tests passed, %i unsupported features",
325+
total - unsupported, unsupported)
305326
return 0
306-
else:
307-
_logger.warning("%i tests passed, %i failures, %i unsupported features", total - (failures + unsupported), failures, unsupported)
308-
return 1
327+
_logger.warning("%i tests passed, %i failures, %i unsupported features",
328+
total - (failures + unsupported), failures, unsupported)
329+
return 1
309330

310331

311332
if __name__ == "__main__":

cwltest/tests/test_categories.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import unittest
22

33
import os
4+
from os import linesep as n
45

56
from .util import run_with_mock_cwl_runner, get_data
67
import xml.etree.ElementTree as ET
@@ -12,16 +13,19 @@ def test_unsupported_with_required_tests(self):
1213
args = ["--test", get_data("tests/test-data/required-unsupported.yml")]
1314
error_code, stdout, stderr = run_with_mock_cwl_runner(args)
1415
self.assertEquals(error_code, 1)
15-
self.assertEquals("Test [1/2] Required test that is unsupported (without tags)\n\n"
16-
"Test [2/2] Required test that is unsupported (with tags)\n\n"
17-
"0 tests passed, 2 failures, 0 unsupported features\n", stderr)
16+
self.assertEquals(
17+
"Test [1/2] Required test that is unsupported (without tags){n}{n}"
18+
"Test [2/2] Required test that is unsupported (with tags){n}{n}"
19+
"0 tests passed, 2 failures, 0 unsupported "
20+
"features{n}".format(n=n), stderr)
1821

1922
def test_unsupported_with_optional_tests(self):
2023
args = ["--test", get_data("tests/test-data/optional-unsupported.yml")]
2124
error_code, stdout, stderr = run_with_mock_cwl_runner(args)
2225
self.assertEquals(error_code, 0)
23-
self.assertEquals("Test [1/1] Optional test that is unsupported\n\n"
24-
"0 tests passed, 1 unsupported features\n", stderr)
26+
self.assertEquals("Test [1/1] Optional test that is unsupported{n}{n}"
27+
"0 tests passed, 1 unsupported "
28+
"features{n}".format(n=n), stderr)
2529

2630
def test_error_with_optional_tests(self):
2731
args = ["--test", get_data("tests/test-data/optional-error.yml")]

cwltest/tests/test_prepare.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import os
2+
import unittest
3+
from cwltest import prepare_test_command
4+
5+
6+
class TestPrepareCommand(unittest.TestCase):
7+
""" Test prepare_test_command() """
8+
9+
def test_unix_relative_path(self):
10+
""" Confirm unix style to windows style path corrections. """
11+
command = prepare_test_command(
12+
tool='cwl-runner',
13+
args=[],
14+
testargs=None,
15+
test={'doc': 'General test of command line generation',
16+
'output': {'args': ['echo']},
17+
'tool': 'v1.0/bwa-mem-tool.cwl',
18+
'job': 'v1.0/bwa-mem-job.json',
19+
'tags': ['required']})
20+
if os.name == 'nt':
21+
self.assertEqual(command[3], 'v1.0\\bwa-mem-tool.cwl')
22+
self.assertEqual(command[4], 'v1.0\\bwa-mem-job.json')
23+
else:
24+
self.assertEqual(command[3], 'v1.0/bwa-mem-tool.cwl')
25+
self.assertEqual(command[4], 'v1.0/bwa-mem-job.json')

0 commit comments

Comments
 (0)