Skip to content

Commit b64c1a4

Browse files
author
Mike Ferris
committed
Merge pull request swiftlang#20 from modocache/lit-tests-v2
[Tests] Add functional tests
2 parents f4c567a + a84cf7f commit b64c1a4

File tree

13 files changed

+478
-1
lines changed

13 files changed

+478
-1
lines changed

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,23 @@ If your install of Swift is located at `/swift` and you wish to install XCTest i
3838
./build_script.py --swiftc="/swift/usr/bin/swiftc" --build-dir="/tmp/XCTest_build" --swift-build-dir="/swift/usr" --library-install-path="/swift/usr/lib/swift/linux" --module-install-path="/swift/usr/lib/swift/linux/x86_64"
3939
```
4040

41+
To run the tests on Linux, pass the `--test` option in combination with options to
42+
install XCTest in your active version of Swift:
43+
44+
```sh
45+
./build_script.py \
46+
--swiftc="/swift/usr/bin/swiftc" \
47+
--build-dir="/tmp/XCTest_build" \
48+
--swift-build-dir="/swift/usr" \
49+
--library-install-path="/swift/usr/lib/swift/linux" \
50+
--module-install-path="/swift/usr/lib/swift/linux/x86_64" \
51+
--test
52+
```
53+
54+
To run the tests on OS X, build and run the `SwiftXCTestFunctionalTests` target in the Xcode project.
55+
56+
You may add tests for XCTest by including them in the `Tests/Functional/` directory. For an example, see `Tests/Functional/SingleFailingTestCase`.
57+
4158
### Additional Considerations for Swift on Linux
4259

4360
When running on the Objective-C runtime, XCTest is able to find all of your tests by simply asking the runtime for the subclasses of `XCTestCase`. It then finds the methods that start with the string `test`. This functionality is not currently present when running on the Swift runtime. Therefore, you must currently provide an additional property called `allTests` in your `XCTestCase` subclass. This method lists all of the tests in the test class. The rest of your test case subclass still contains your test methods.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// RUN: %{swiftc} %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 : XCTAssertTrue failed -
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) || os(FreeBSD)
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+
# We'll provide one via the %swiftc substitution.
17+
#
18+
# Linux tests are run after swift-corelibs-xctest is installed
19+
# in the Swift library path, so we only need the path to `swiftc`
20+
# in order to compile.
21+
def _getenv(name):
22+
value = os.getenv(name, None)
23+
if value is None:
24+
lit_config.fatal(
25+
'Environment variable ${} is not set.\n'
26+
'$SWIFT_EXEC, $SDKROOT, $BUILT_PRODUCTS_DIR, $PLATFORM_NAME, and '
27+
'$MACOSX_DEPLOYMENT_TARGET must all be set for lit tests to '
28+
'run.'.format(name))
29+
return value
30+
31+
swift_exec = [_getenv('SWIFT_EXEC')]
32+
33+
if platform.system() == 'Darwin':
34+
# On OS X, we need to make sure swiftc references the
35+
# proper SDK, has a deployment target set, and more...
36+
# Here we rely on environment variables, produced by xcodebuild.
37+
sdk_root = _getenv('SDKROOT')
38+
built_products_dir = _getenv('BUILT_PRODUCTS_DIR')
39+
platform_name = _getenv('PLATFORM_NAME')
40+
deployment_target = _getenv('MACOSX_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(('%{swiftc}', ' '.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: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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_too_few_expected_raises(self):
23+
actual = _tmpfile('foo\nbar\nbaz\n')
24+
expected = _tmpfile('c: foo\nc: bar\n')
25+
with self.assertRaises(AssertionError):
26+
compare.compare(actual, expected, check_prefix='c: ')
27+
28+
def test_too_many_expected_raises(self):
29+
actual = _tmpfile('foo\nbar\n')
30+
expected = _tmpfile('c: foo\nc: bar\nc: baz\n')
31+
with self.assertRaises(AssertionError):
32+
compare.compare(actual, expected, check_prefix='c: ')
33+
34+
def test_match_does_not_raise(self):
35+
actual = _tmpfile('foo\nbar\nbaz\n')
36+
expected = _tmpfile('c: foo\nc: bar\nc: baz\n')
37+
compare.compare(actual, expected, check_prefix='c: ')
38+
39+
40+
if __name__ == "__main__":
41+
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: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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. Also raises an AssertionError if the number
29+
of lines in the two files differ.
30+
"""
31+
for actual_line, expected_line in map(
32+
None,
33+
_actual_lines(actual),
34+
_expected_lines(expected, check_prefix)):
35+
if actual_line is None:
36+
raise AssertionError('There were more lines expected to appear '
37+
'than there were lines in the actual input.')
38+
if expected_line is None:
39+
raise AssertionError('There were more lines than expected to '
40+
'appear.')
41+
if not re.match(expected_line, actual_line):
42+
raise AssertionError('Actual line did not match the expected '
43+
'regular expression.\n'
44+
'Actual: {}\n'
45+
'Expected: {}\n'.format(
46+
repr(actual_line), repr(expected_line)))
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)