Skip to content

Commit 8890f6d

Browse files
committed
[Tests] Add functional tests
Adds a test runner (lit) that compares annotations in the source code of an XCTest file to actual output when that source code is compiled and run. This acts as a regression test suite for the project. - Because FileCheck is not available unless LLVM is built, includes an XCTest-specific version of FileCheck called `xctest_checker`. This is used by the lit tests in order to verify the output of XCTest runs. - Adds a test case that verifies the output of a single failing test case. - To run the tests on Linux, adds a `--test` option to `build_script.py`. - To run the tests on OS X, adds a `SwiftXCTestFunctionalTests` target to the Xcode project.
1 parent 8dce296 commit 8890f6d

File tree

12 files changed

+372
-2
lines changed

12 files changed

+372
-2
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// RUN: %swift_exec %s -o %built_tests_dir/SingleFailingTestCase
2+
// RUN: %built_tests_dir/SingleFailingTestCase > %test_output || true
3+
// RUN: %xctest-checker %test_output %s
4+
// CHECK: Test Case 'SingleFailingTestCase.test_fails' started.
5+
// CHECK: .*/Tests/Functional/SingleFailingTestCase/main.swift:24: error: SingleFailingTestCase.test_fails :
6+
// CHECK: Test Case 'SingleFailingTestCase.test_fails' failed \(\d+\.\d+ seconds\).
7+
// CHECK: Executed 1 test, with 1 failure 0 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds
8+
// CHECK: Total executed 1 test, with 1 failure \(0 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds
9+
10+
#if os(Linux)
11+
import XCTest
12+
#else
13+
import SwiftXCTest
14+
#endif
15+
16+
class SingleFailingTestCase: XCTestCase {
17+
var allTests: [(String, () -> ())] {
18+
return [
19+
("test_fails", test_fails),
20+
]
21+
}
22+
23+
func test_fails() {
24+
XCTAssert(false)
25+
}
26+
}
27+
28+
XCTMain([SingleFailingTestCase()])

Tests/Functional/lit.cfg

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import os
2+
import platform
3+
import tempfile
4+
5+
import lit
6+
7+
# Set up lit config.
8+
config.name = 'SwiftXCTestFunctionalTests'
9+
config.test_format = lit.formats.ShTest(execute_external=False)
10+
config.suffixes = ['.swift']
11+
12+
# Set up the substitutions used by the functional test suite.
13+
14+
# First, our tests need a way to compile source files into
15+
# executables that are linked against swift-corelibs-xctest.
16+
#
17+
# Linux tests are run after swift-corelibs-xctest is installed
18+
# in the Swift library path, so we only need the path to `swiftc`
19+
# in order to compile.
20+
swift_exec = [os.getenv('SWIFT_EXEC')]
21+
assert swift_exec[0] is not None, \
22+
'Environment variable $SWIFT_EXEC must be set.'
23+
24+
if platform.system() == 'Darwin':
25+
# On OS X, we need to make sure swiftc references the
26+
# proper SDK, has a deployment target set, and more...
27+
# Here we rely on environment variables, produced by xcodebuild.
28+
sdk_root = os.getenv('SDKROOT')
29+
built_products_dir = os.getenv('BUILT_PRODUCTS_DIR')
30+
platform_name = os.getenv('PLATFORM_NAME')
31+
deployment_target = os.getenv('MACOSX_DEPLOYMENT_TARGET')
32+
assert sdk_root and built_products_dir and platform_name and deployment_target, \
33+
'Environment variables $SDKROOT, $BUILT_PRODUCTS_DIR, ' + \
34+
'$PLATFORM_NAME, and $MACOSX_DEPLOYMENT_TARGET must all be set.\n' + \
35+
'$SDKROOT: "{sdk_root}"\n' + \
36+
'$BUILT_PRODUCTS_DIR: "{built_products_dir}"\n' + \
37+
'$PLATFORM_NAME: "{platform_name}"\n' + \
38+
'$MACOSX_DEPLOYMENT_TARGET: "{deployment_target}"\n'.format(
39+
sdk_root=sdk_root, built_products_dir=built_products_dir,
40+
platform_name=platform_name, deployment_target=deployment_target)
41+
42+
target = '{}-apple-{}{}'.format(
43+
platform.machine(), platform_name, deployment_target)
44+
swift_exec.extend([
45+
'-sdk', sdk_root,
46+
'-target', target,
47+
'-L', built_products_dir,
48+
'-I', built_products_dir,
49+
'-F', built_products_dir,
50+
'-Xlinker', '-rpath',
51+
'-Xlinker', built_products_dir])
52+
53+
# Having prepared the swiftc command, we set the substitution.
54+
config.substitutions.append(('%swift_exec', ' '.join(swift_exec)))
55+
56+
# Add the %built_tests_dir substitution, which is a temporary
57+
# directory used to store built files.
58+
built_tests_dir = tempfile.mkdtemp()
59+
config.substitutions.append(('%built_tests_dir', built_tests_dir))
60+
61+
# Add the %test_output substitution, which is a temporary file
62+
# used to store test output.
63+
test_output = tempfile.mkstemp()[1]
64+
config.substitutions.append(('%test_output', test_output))
65+
66+
# Add the %xctest-checker substitution, which is a Python script
67+
# can be used to compare the actual XCTest output to the expected
68+
# output.
69+
xctest_checker = os.path.join(
70+
os.path.dirname(os.path.abspath(__file__)),
71+
'xctest_checker',
72+
'xctest_checker.py')
73+
config.substitutions.append(('%xctest-checker', xctest_checker))
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Compile artifacts
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
6+
# Distribution/packaging
7+
.Python
8+
env/
9+
build/
10+
develop-eggs/
11+
dist/
12+
downloads/
13+
eggs/
14+
.eggs/
15+
lib/
16+
lib64/
17+
parts/
18+
sdist/
19+
var/
20+
*.egg-info/
21+
.installed.cfg
22+
*.egg
23+
24+
# Installer logs
25+
pip-log.txt
26+
pip-delete-this-directory.txt
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import os
2+
import setuptools
3+
4+
import xctest_checker
5+
6+
# setuptools expects to be invoked from within the directory of setup.py,
7+
# but it is nice to allow `python path/to/setup.py install` to work
8+
# (for scripts, etc.)
9+
os.chdir(os.path.dirname(os.path.abspath(__file__)))
10+
11+
setuptools.setup(
12+
name='xctest_checker',
13+
version=xctest_checker.__version__,
14+
15+
author=xctest_checker.__author__,
16+
author_email=xctest_checker.__email__,
17+
url='http://swift.org',
18+
license='Apache',
19+
20+
description="A tool to verify the output of XCTest runs.",
21+
keywords='test xctest swift',
22+
23+
test_suite='tests',
24+
25+
classifiers=[
26+
'Development Status :: 3 - Alpha',
27+
'Environment :: Console',
28+
'Intended Audience :: Developers',
29+
'License :: OSI Approved :: Apache Software License',
30+
'Natural Language :: English',
31+
'Operating System :: OS Independent',
32+
'Programming Language :: Python',
33+
'Topic :: Software Development :: Testing',
34+
],
35+
36+
zip_safe=False,
37+
packages=setuptools.find_packages(),
38+
entry_points={
39+
'console_scripts': [
40+
'xctest_checker = xctest_checker:main',
41+
],
42+
}
43+
)

Tests/Functional/xctest_checker/tests/__init__.py

Whitespace-only changes.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import tempfile
2+
import unittest
3+
4+
from xctest_checker import compare
5+
6+
7+
def _tmpfile(content):
8+
"""Returns the path to a temp file with the given contents."""
9+
tmp = tempfile.mkstemp()[1]
10+
with open(tmp, 'w') as f:
11+
f.write(content)
12+
return tmp
13+
14+
15+
class CompareTestCase(unittest.TestCase):
16+
def test_no_match_raises(self):
17+
actual = _tmpfile('foo\nbar\nbaz\n')
18+
expected = _tmpfile('c: foo\nc: baz\nc: bar\n')
19+
with self.assertRaises(AssertionError):
20+
compare.compare(actual, expected, check_prefix='c: ')
21+
22+
def test_match_does_not_raise(self):
23+
actual = _tmpfile('foo\nbar\nbaz\n')
24+
expected = _tmpfile('c: foo\nc: bar\ndoes not match but not checked\n')
25+
compare.compare(actual, expected, check_prefix='c: ')
26+
27+
28+
if __name__ == "__main__":
29+
unittest.main()
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/usr/bin/env python
2+
3+
import xctest_checker
4+
5+
if __name__ == '__main__':
6+
xctest_checker.main()
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from __future__ import absolute_import
2+
from .main import main
3+
4+
__author__ = 'Brian Gesiak'
5+
__email__ = '[email protected]'
6+
__versioninfo__ = (0, 1, 0)
7+
__version__ = '.'.join(str(v) for v in __versioninfo__)
8+
9+
__all__ = []
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import re
2+
3+
4+
def _actual_lines(path):
5+
"""
6+
Returns a generator that yields each line in the file at the given path.
7+
"""
8+
with open(path) as f:
9+
for line in f:
10+
yield line
11+
12+
13+
def _expected_lines(path, check_prefix):
14+
"""
15+
Returns a generator that yields each line in the file at the given path
16+
that begins with the given prefix.
17+
"""
18+
with open(path) as f:
19+
for line in f:
20+
if line.startswith(check_prefix):
21+
yield line[len(check_prefix):]
22+
23+
24+
def compare(actual, expected, check_prefix):
25+
"""
26+
Compares each line in the two given files.
27+
If any line in the 'actual' file doesn't match the regex in the 'expected'
28+
file, raises an AssertionError.
29+
"""
30+
for lines in zip(
31+
_actual_lines(actual),
32+
_expected_lines(expected, check_prefix)):
33+
if not re.match(lines[1], lines[0]):
34+
raise AssertionError('Actual line did not match the expected '
35+
'regular expression.\n'
36+
'Actual: {}\n'
37+
'Expected: {}\n'.format(
38+
repr(lines[0]), repr(lines[1])))
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#!/usr/bin/env python
2+
3+
from __future__ import absolute_import
4+
5+
import argparse
6+
7+
from . import compare
8+
9+
10+
def main():
11+
parser = argparse.ArgumentParser()
12+
parser.add_argument('actual', help='A path to a file containing the '
13+
'actual output of an XCTest run.')
14+
parser.add_argument('expected', help='A path to a file containing the '
15+
'expected output of an XCTest run.')
16+
parser.add_argument('-p', '--check-prefix',
17+
default='// CHECK: ',
18+
help='{prog} checks actual output against expected '
19+
'output. By default, {prog} only checks lines '
20+
'that are prefixed with "// CHECK: ". This '
21+
'option can be used to change that '
22+
'prefix.'.format(prog=parser.prog))
23+
args = parser.parse_args()
24+
compare.compare(args.actual, args.expected, args.check_prefix)
25+
26+
27+
if __name__ == '__main__':
28+
main()

0 commit comments

Comments
 (0)