diff --git a/.travis.yml b/.travis.yml index 42ca4116fce6f..e5a1bda55597a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -104,15 +104,4 @@ script: - echo "script start" - source activate pandas-dev - ci/build_docs.sh - - ci/run_tests.sh - -after_script: - - echo "after_script start" - - source activate pandas-dev && pushd /tmp && python -c "import pandas; pandas.show_versions();" && popd - - if [ -e test-data-single.xml ]; then - ci/print_skipped.py test-data-single.xml; - fi - - if [ -e test-data-multiple.xml ]; then - ci/print_skipped.py test-data-multiple.xml; - fi - - echo "after_script done" + - python ci/run_tests.py diff --git a/ci/azure/posix.yml b/ci/azure/posix.yml index c0c4fb924a605..87534170f3230 100644 --- a/ci/azure/posix.yml +++ b/ci/azure/posix.yml @@ -43,7 +43,7 @@ jobs: ENV_FILE: ci/deps/azure-37-numpydev.yaml CONDA_PY: "37" PATTERN: "not slow and not network and not db" - TEST_ARGS: "-W error" + WARNINGS_ARE_ERRORS: "true" PANDAS_TESTING_MODE: "deprecate" EXTRA_APT: "xsel" @@ -64,33 +64,21 @@ jobs: - script: | export PATH=$HOME/miniconda3/bin:$PATH source activate pandas-dev - ci/run_tests.sh + python ci/run_tests.py displayName: 'Test' - script: | export PATH=$HOME/miniconda3/bin:$PATH source activate pandas-dev && pushd /tmp && python -c "import pandas; pandas.show_versions();" && popd - task: PublishTestResults@2 inputs: - testResultsFiles: 'test-data-*.xml' + testResultsFiles: 'test-data.xml' testRunTitle: ${{ format('{0}-$(CONDA_PY)', parameters.name) }} - powershell: | - $junitXml = "test-data-single.xml" + $junitXml = "test-data.xml" $(Get-Content $junitXml | Out-String) -match 'failures="(.*?)"' if ($matches[1] -eq 0) { - Write-Host "No test failures in test-data-single" - } - else - { - # note that this will produce $LASTEXITCODE=1 - Write-Error "$($matches[1]) tests failed" - } - - $junitXmlMulti = "test-data-multiple.xml" - $(Get-Content $junitXmlMulti | Out-String) -match 'failures="(.*?)"' - if ($matches[1] -eq 0) - { - Write-Host "No test failures in test-data-multi" + Write-Host "No test failures in test-data" } else { diff --git a/ci/azure/windows.yml b/ci/azure/windows.yml index f06b229bb2656..c5d25aa03824b 100644 --- a/ci/azure/windows.yml +++ b/ci/azure/windows.yml @@ -11,10 +11,12 @@ jobs: py36_np14: ENV_FILE: ci/deps/azure-windows-36.yaml CONDA_PY: "36" + PATTERN: "not slow and not network and not db" py27_np121: ENV_FILE: ci/deps/azure-windows-27.yaml CONDA_PY: "27" + PATTERN: "not slow and not network and not db" steps: - task: CondaEnvironment@1 @@ -38,7 +40,7 @@ jobs: displayName: 'Build' - script: | call activate pandas-dev - pytest -m "not slow and not network and not db" --junitxml=test-data.xml pandas -n 2 -r sxX --strict --durations=10 %* + python ci\\run_tests.py displayName: 'Test' - task: PublishTestResults@2 inputs: diff --git a/ci/print_skipped.py b/ci/print_skipped.py deleted file mode 100755 index 67bc7b556cd43..0000000000000 --- a/ci/print_skipped.py +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env python - -import sys -import math -import xml.etree.ElementTree as et - - -def parse_results(filename): - tree = et.parse(filename) - root = tree.getroot() - skipped = [] - - current_class = '' - i = 1 - assert i - 1 == len(skipped) - for el in root.findall('testcase'): - cn = el.attrib['classname'] - for sk in el.findall('skipped'): - old_class = current_class - current_class = cn - name = '{classname}.{name}'.format(classname=current_class, - name=el.attrib['name']) - msg = sk.attrib['message'] - out = '' - if old_class != current_class: - ndigits = int(math.log(i, 10) + 1) - - # 4 for : + space + # + space - out += ('-' * (len(name + msg) + 4 + ndigits) + '\n') - out += '#{i} {name}: {msg}'.format(i=i, name=name, msg=msg) - skipped.append(out) - i += 1 - assert i - 1 == len(skipped) - assert i - 1 == len(skipped) - # assert len(skipped) == int(root.attrib['skip']) - return '\n'.join(skipped) - - -def main(args): - print('SKIPPED TESTS:') - for fn in args.filename: - print(parse_results(fn)) - return 0 - - -def parse_args(): - import argparse - parser = argparse.ArgumentParser() - parser.add_argument('filename', nargs='+', help='XUnit file to parse') - return parser.parse_args() - - -if __name__ == '__main__': - sys.exit(main(parse_args())) diff --git a/ci/run_tests.py b/ci/run_tests.py new file mode 100755 index 0000000000000..1d186f0469039 --- /dev/null +++ b/ci/run_tests.py @@ -0,0 +1,143 @@ +#!/bin/env python +import os +import subprocess +import sys +import random +import tempfile +import time +import warnings +import xml.etree.ElementTree +try: + from urllib.request import urlretrieve +except ImportError: # py2 + from urllib import urlretrieve + + +def set_environ(pattern, locale): + """ + Set environment variables needed for running the tests. + """ + # Workaround for pytest-xdist flaky collection order + # https://github.com/pytest-dev/pytest/issues/920 + # https://github.com/pytest-dev/pytest/issues/1075 + os.environ['PYTHONHASHSEED'] = str(random.randint(1, 4294967295)) + + if locale: + os.environ['LC_ALL'] = os.environ['LANG'] = locale + import pandas + pandas_locale = pandas.get_option('display.encoding') + if pandas_locale != locale: + # TODO raise exception instead of warning when + # https://github.com/pandas-dev/pandas/issues/23923 is fixed + warnings.warn(('pandas could not detect the locale. ' + 'System locale: {}, ' + 'pandas detected: {}').format(locale, + pandas_locale)) + + if 'not network' in pattern: + os.environ['http_proxy'] = os.environ['https_proxy'] = 'http://1.2.3.4' + + +def skipped_tests(fname): + """ + Yield the list of skipped tests, including a header to be printed. + """ + root = xml.etree.ElementTree.parse(fname).getroot() + for item in root.findall('testcase'): + for skipped in item.findall('skipped'): + yield (item.attrib['classname'], + item.attrib['name'], + skipped.attrib['message']) + + +def pytest_command(pattern, junit_xml, coverage_file): + """ + Build and return the pytest command to run. + """ + cmd = ['pytest', '--junitxml={}'.format(junit_xml)] + + if pattern: + cmd += ['-m', pattern] + + if coverage_file: + cmd += ['--cov=pandas', '--cov-report=xml:{}'.format(coverage_file)] + + test_jobs = os.environ.get('TESTS_JOBS', 'auto') + if test_jobs != '0': + cmd += ['-n', test_jobs, '--dist', 'loadfile'] + + if os.environ.get('WARNINGS_ARE_ERRORS'): + cmd += ['-W', 'error'] + + return cmd + ['pandas'] + + +def upload_coverage(coverage_file): + """ + Download codecov.io script and run it to upload coverage for coverage_file. + """ + script_fname = os.path.join(os.path.dirname(coverage_file), + 'codecov_script.sh') + urlretrieve('https://codecov.io/bash', script_fname) + upload_coverage_cmd = ['bash', + script_fname, + '-Z', + '-c', + '-f', + coverage_file] + sys.stderr.write('{}\n'.format(' '.join(upload_coverage_cmd))) + subprocess.check_call(upload_coverage_cmd.split()) + os.remove(script_fname) + os.remove(coverage_file) + + +def run_tests(pattern, locale=None, coverage_file=False): + """ + Run tests with the specified environment. + + Parameters + ---------- + pattern : str + Tests to execute based on pytest markers (e.g. "slow and not network"). + locale : str, optional + Locale to use instead of the system defaule (e.g. "it_IT.UTF8"). + coverage_file : str, optional + If provided, the file path where to save the coverage. + """ + if os.environ.get('DOC'): + sys.stdout.write('We are not running pytest as this is a doc-build\n') + return + junit_xml = 'test-data.xml' + set_environ(pattern, locale) + pytest_cmd = pytest_command(pattern, junit_xml, coverage_file) + sys.stderr.write('{}\n'.format(' '.join(pytest_cmd))) + start = time.time() + subprocess.check_call(pytest_cmd) + tests_run_in_seconds = int(time.time() - start) + + prev_class = None + for i, (class_, name, msg) in enumerate(skipped_tests(junit_xml)): + if prev_class is not None and class_ != prev_class: + sys.stdout.write('{}\n'.format('-' * 100)) + sys.stdout.write('#{} {}.{}: {}\n'.format(i + 1, class_, name, msg)) + prev_class = class_ + sys.stdout.write('{}\n'.format('=' * 100)) + import pandas + pandas.show_versions() + sys.stdout.write('{}\n'.format('=' * 100)) + sys.stdout.write('Tests run in {} seconds\n'.format(tests_run_in_seconds)) + + if coverage_file: + upload_coverage(coverage_file) + + +if __name__ == '__main__': + pattern = os.environ.get('PATTERN', '') + locale = os.environ.get('LOCALE_OVERRIDE') + coverage_file = None + if os.environ.get('COVERAGE', '') != '': + if sys.platform == 'win32': + raise RuntimeError('Coverage can not be uploaded from Windows') + coverage_file = os.path.join(tempfile.gettempdir(), + 'pandas-coverage.xml') + run_tests(pattern, locale, coverage_file) diff --git a/ci/run_tests.sh b/ci/run_tests.sh deleted file mode 100755 index ee46da9f52eab..0000000000000 --- a/ci/run_tests.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/bin/bash - -set -e - -if [ "$DOC" ]; then - echo "We are not running pytest as this is a doc-build" - exit 0 -fi - -# Workaround for pytest-xdist flaky collection order -# https://github.com/pytest-dev/pytest/issues/920 -# https://github.com/pytest-dev/pytest/issues/1075 -export PYTHONHASHSEED=$(python -c 'import random; print(random.randint(1, 4294967295))') - -if [ -n "$LOCALE_OVERRIDE" ]; then - export LC_ALL="$LOCALE_OVERRIDE" - export LANG="$LOCALE_OVERRIDE" - PANDAS_LOCALE=`python -c 'import pandas; pandas.get_option("display.encoding")'` - if [[ "$LOCALE_OVERIDE" != "$PANDAS_LOCALE" ]]; then - echo "pandas could not detect the locale. System locale: $LOCALE_OVERRIDE, pandas detected: $PANDAS_LOCALE" - # TODO Not really aborting the tests until https://github.com/pandas-dev/pandas/issues/23923 is fixed - # exit 1 - fi -fi -if [[ "not network" == *"$PATTERN"* ]]; then - export http_proxy=http://1.2.3.4 https_proxy=http://1.2.3.4; -fi - - -if [ -n "$PATTERN" ]; then - PATTERN=" and $PATTERN" -fi - -for TYPE in single multiple -do - if [ "$COVERAGE" ]; then - COVERAGE_FNAME="/tmp/coc-$TYPE.xml" - COVERAGE="-s --cov=pandas --cov-report=xml:$COVERAGE_FNAME" - fi - - TYPE_PATTERN=$TYPE - NUM_JOBS=1 - if [[ "$TYPE_PATTERN" == "multiple" ]]; then - TYPE_PATTERN="not single" - NUM_JOBS=2 - fi - - PYTEST_CMD="pytest -m \"$TYPE_PATTERN$PATTERN\" -n $NUM_JOBS -s --strict --durations=10 --junitxml=test-data-$TYPE.xml $TEST_ARGS $COVERAGE pandas" - echo $PYTEST_CMD - # if no tests are found (the case of "single and slow"), pytest exits with code 5, and would make the script fail, if not for the below code - sh -c "$PYTEST_CMD; ret=\$?; [ \$ret = 5 ] && exit 0 || exit \$ret" - - if [[ "$COVERAGE" && $? == 0 ]]; then - echo "uploading coverage for $TYPE tests" - echo "bash <(curl -s https://codecov.io/bash) -Z -c -F $TYPE -f $COVERAGE_FNAME" - bash <(curl -s https://codecov.io/bash) -Z -c -F $TYPE -f $COVERAGE_FNAME - fi -done diff --git a/pandas/tests/frame/test_rank.py b/pandas/tests/frame/test_rank.py index e7a876bcf52d1..0e3b60212bf42 100644 --- a/pandas/tests/frame/test_rank.py +++ b/pandas/tests/frame/test_rank.py @@ -310,7 +310,7 @@ def test_rank_pct_true(self, method, exp): expected = DataFrame(exp) tm.assert_frame_equal(result, expected) - @pytest.mark.single + @pytest.mark.serial def test_pct_max_many_rows(self): # GH 18271 df = DataFrame({'A': np.arange(2**24 + 1), diff --git a/pandas/tests/io/json/test_pandas.py b/pandas/tests/io/json/test_pandas.py index 3fdf303ea2e8e..8a93fb7a83d0a 100644 --- a/pandas/tests/io/json/test_pandas.py +++ b/pandas/tests/io/json/test_pandas.py @@ -846,7 +846,7 @@ def test_misc_example(self): assert_frame_equal(result, expected) @network - @pytest.mark.single + @pytest.mark.serial def test_round_trip_exception_(self): # GH 3867 csv = 'https://raw.github.com/hayd/lahman2012/master/csvs/Teams.csv' @@ -857,7 +857,7 @@ def test_round_trip_exception_(self): index=df.index, columns=df.columns), df) @network - @pytest.mark.single + @pytest.mark.serial def test_url(self): url = 'https://api.github.com/repos/pandas-dev/pandas/issues?per_page=5' # noqa result = read_json(url, convert_dates=True) diff --git a/pandas/tests/io/test_clipboard.py b/pandas/tests/io/test_clipboard.py index bb73c6bc6b38b..f44b63c46f99c 100644 --- a/pandas/tests/io/test_clipboard.py +++ b/pandas/tests/io/test_clipboard.py @@ -115,7 +115,7 @@ def test_mock_clipboard(mock_clipboard): assert result == "abc" -@pytest.mark.single +@pytest.mark.serial @pytest.mark.clipboard @pytest.mark.skipif(not _DEPS_INSTALLED, reason="clipboard primitives not installed") diff --git a/pandas/tests/io/test_feather.py b/pandas/tests/io/test_feather.py index 44d642399ced9..f3c3b70d78c66 100644 --- a/pandas/tests/io/test_feather.py +++ b/pandas/tests/io/test_feather.py @@ -15,7 +15,7 @@ pyarrow_version = LooseVersion(pyarrow.__version__) -@pytest.mark.single +@pytest.mark.serial class TestFeather(object): def check_error_on_write(self, df, exc): diff --git a/pandas/tests/io/test_gbq.py b/pandas/tests/io/test_gbq.py index 6dd16107bc7d7..0f8f6a3358f6f 100644 --- a/pandas/tests/io/test_gbq.py +++ b/pandas/tests/io/test_gbq.py @@ -109,7 +109,7 @@ def test_read_gbq_without_dialect_warns_future_change(monkeypatch): pd.read_gbq("SELECT 1") -@pytest.mark.single +@pytest.mark.serial class TestToGBQIntegrationWithServiceAccountKeyPath(object): @classmethod diff --git a/pandas/tests/io/test_pytables.py b/pandas/tests/io/test_pytables.py index 1c4d00c8b3e15..17a12d3705b1d 100644 --- a/pandas/tests/io/test_pytables.py +++ b/pandas/tests/io/test_pytables.py @@ -142,7 +142,7 @@ def teardown_method(self, method): pass -@pytest.mark.single +@pytest.mark.serial @pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") class TestHDFStore(Base): diff --git a/pandas/tests/io/test_sql.py b/pandas/tests/io/test_sql.py index c202fae8c91cf..887e68ad6c96d 100644 --- a/pandas/tests/io/test_sql.py +++ b/pandas/tests/io/test_sql.py @@ -886,7 +886,7 @@ def test_escaped_table_name(self): tm.assert_frame_equal(res, df) -@pytest.mark.single +@pytest.mark.serial class TestSQLApi(SQLAlchemyMixIn, _TestSQLApi): """ Test the public API as it would be used directly @@ -1077,12 +1077,12 @@ def setup_method(self, load_iris_data): # super(_EngineToConnMixin, self).teardown_method(method) -@pytest.mark.single +@pytest.mark.serial class TestSQLApiConn(_EngineToConnMixin, TestSQLApi): pass -@pytest.mark.single +@pytest.mark.serial class TestSQLiteFallbackApi(SQLiteMixIn, _TestSQLApi): """ Test the public sqlite connection fallback API @@ -1978,36 +1978,36 @@ def psql_insert_copy(table, conn, keys, data_iter): tm.assert_frame_equal(result, expected) -@pytest.mark.single +@pytest.mark.serial @pytest.mark.db class TestMySQLAlchemy(_TestMySQLAlchemy, _TestSQLAlchemy): pass -@pytest.mark.single +@pytest.mark.serial @pytest.mark.db class TestMySQLAlchemyConn(_TestMySQLAlchemy, _TestSQLAlchemyConn): pass -@pytest.mark.single +@pytest.mark.serial @pytest.mark.db class TestPostgreSQLAlchemy(_TestPostgreSQLAlchemy, _TestSQLAlchemy): pass -@pytest.mark.single +@pytest.mark.serial @pytest.mark.db class TestPostgreSQLAlchemyConn(_TestPostgreSQLAlchemy, _TestSQLAlchemyConn): pass -@pytest.mark.single +@pytest.mark.serial class TestSQLiteAlchemy(_TestSQLiteAlchemy, _TestSQLAlchemy): pass -@pytest.mark.single +@pytest.mark.serial class TestSQLiteAlchemyConn(_TestSQLiteAlchemy, _TestSQLAlchemyConn): pass @@ -2015,7 +2015,7 @@ class TestSQLiteAlchemyConn(_TestSQLiteAlchemy, _TestSQLAlchemyConn): # ----------------------------------------------------------------------------- # -- Test Sqlite / MySQL fallback -@pytest.mark.single +@pytest.mark.serial class TestSQLiteFallback(SQLiteMixIn, PandasSQLTest): """ Test the fallback mode against an in-memory sqlite database. @@ -2240,7 +2240,7 @@ def tquery(query, con=None, cur=None): return list(res) -@pytest.mark.single +@pytest.mark.serial class TestXSQLite(SQLiteMixIn): @pytest.fixture(autouse=True) @@ -2447,7 +2447,7 @@ def clean_up(test_table_to_drop): clean_up(table_name) -@pytest.mark.single +@pytest.mark.serial @pytest.mark.db @pytest.mark.skip(reason="gh-13611: there is no support for MySQL " "if SQLAlchemy is not installed") diff --git a/pandas/tests/series/test_rank.py b/pandas/tests/series/test_rank.py index da414a577ae0b..8cae89997c9df 100644 --- a/pandas/tests/series/test_rank.py +++ b/pandas/tests/series/test_rank.py @@ -496,7 +496,7 @@ def test_rank_first_pct(dtype, ser, exp): assert_series_equal(result, expected) -@pytest.mark.single +@pytest.mark.serial def test_pct_max_many_rows(): # GH 18271 s = Series(np.arange(2**24 + 1)) diff --git a/pandas/tests/test_algos.py b/pandas/tests/test_algos.py index c9d403f6696af..36246491c66a9 100644 --- a/pandas/tests/test_algos.py +++ b/pandas/tests/test_algos.py @@ -1470,7 +1470,7 @@ def test_too_many_ndims(self): with pytest.raises(TypeError, match=msg): algos.rank(arr) - @pytest.mark.single + @pytest.mark.serial @pytest.mark.parametrize('values', [ np.arange(2**24 + 1), np.arange(2**25 + 2).reshape(2**24 + 1, 2)], diff --git a/setup.cfg b/setup.cfg index fceff01f0671f..2db53ca7840bf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -64,14 +64,14 @@ split_penalty_logical_operator = 30 [tool:pytest] testpaths = pandas markers = - single: mark a test as single cpu only + serial: mark a test as single cpu only slow: mark a test as slow network: mark a test as network db: tests requiring a database (mysql or postgres) high_memory: mark a test as a high-memory only clipboard: mark a pd.read_clipboard test doctest_optionflags = NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL -addopts = --strict-data-files +addopts = --strict --strict-data-files --durations=10 -r sxX --capture=no xfail_strict = True [coverage:run] diff --git a/test.bat b/test.bat deleted file mode 100644 index e07c84f257a69..0000000000000 --- a/test.bat +++ /dev/null @@ -1,3 +0,0 @@ -:: test on windows - -pytest --skip-slow --skip-network pandas -n 2 -r sxX --strict %* diff --git a/test.sh b/test.sh deleted file mode 100755 index 1255a39816f78..0000000000000 --- a/test.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -command -v coverage >/dev/null && coverage erase -command -v python-coverage >/dev/null && python-coverage erase -pytest pandas --cov=pandas -r sxX --strict diff --git a/test_fast.bat b/test_fast.bat deleted file mode 100644 index 81f30dd310e28..0000000000000 --- a/test_fast.bat +++ /dev/null @@ -1,3 +0,0 @@ -:: test on windows -set PYTHONHASHSEED=314159265 -pytest --skip-slow --skip-network -m "not single" -n 4 -r sXX --strict pandas diff --git a/test_fast.sh b/test_fast.sh deleted file mode 100755 index 1fb55e581d292..0000000000000 --- a/test_fast.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -# Workaround for pytest-xdist flaky collection order -# https://github.com/pytest-dev/pytest/issues/920 -# https://github.com/pytest-dev/pytest/issues/1075 -export PYTHONHASHSEED=$(python -c 'import random; print(random.randint(1, 4294967295))') - -pytest pandas --skip-slow --skip-network -m "not single" -n 4 -r sxX --strict "$@" diff --git a/test_rebuild.sh b/test_rebuild.sh deleted file mode 100755 index 65aa1098811a1..0000000000000 --- a/test_rebuild.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh - -python setup.py clean -python setup.py build_ext --inplace -coverage erase -pytest pandas --cov=pandas