Skip to content

Commit

Permalink
TST: Switch to pytest, simplify CI (#225)
Browse files Browse the repository at this point in the history
* Switch nose to pytest (nose is deprecated)
* Remove conda metachannel code path (macOS) on travis because it doesn't seem to impact performance
* Switch to Xenial on all travis tests: https://docs.travis-ci.com/user/reference/xenial/#differences-from-the-trusty-images
  • Loading branch information
bocklund authored and richardotis committed Jul 19, 2019
1 parent 21be98a commit 74d8917
Show file tree
Hide file tree
Showing 8 changed files with 86 additions and 124 deletions.
10 changes: 0 additions & 10 deletions .coveragerc

This file was deleted.

30 changes: 11 additions & 19 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ notifications:

matrix:
include:
- python: 2.7
- python: 3.6
- name: "Python 2.7.15 on Xenial Linux"
python: 2.7 # this works for Linux but is ignored on macOS or Windows
dist: xenial
- name: "Python 3.6.1 on Xenial Linux"
python: 3.6 # this works for Linux but is ignored on macOS or Windows
dist: xenial
env: DEPLOY_ENC_LABEL=e64cfe3b4e81
- name: "Python 3.7.1 on Xenial Linux"
python: 3.7 # this works for Linux but is ignored on macOS or Windows
dist: xenial # required for Python >= 3.7
dist: xenial
- language: generic
os: osx
env: TRAVIS_PYTHON_VERSION=2.7
Expand All @@ -34,20 +38,8 @@ install:
. $HOME/miniconda2/etc/profile.d/conda.sh
conda deactivate
conda activate condaenv
# Use a conda-forge metachannel for speed, including only dependencies
# of pycalphad. See https://github.com/regro/conda-metachannel
# For some reason, conda-metachannel is breaking Linux by not picking
# up some packages (e.g. libgfortran-ng), which I think come from
# defaults. Only use conda metachannel on Mac.
if [ "$TRAVIS_OS_NAME" == "osx" ]; then
echo "!!! Installing pycalphad dependencies via conda metachannel"
conda install --yes --override-channels -c https://metachannel.conda-forge.org/conda-forge/nose,pycalphad,python-symengine python=$TRAVIS_PYTHON_VERSION numpy scipy matplotlib nose pandas sympy pyparsing dask dill xarray cython cyipopt symengine python-symengine
else
echo "!!! Installing pycalphad dependencies via conda the normal way"
# I think the problem is that libgfortran-ng from defaults is not picked up. add those to the metachannel
conda install --yes python=$TRAVIS_PYTHON_VERSION gxx_linux-64 numpy scipy matplotlib nose pandas sympy pyparsing dask dill python-symengine
conda install --yes xarray cython cyipopt
fi
echo "!!! Installing pycalphad dependencies via conda"
conda install --yes python=$TRAVIS_PYTHON_VERSION numpy scipy matplotlib pytest pytest-cov pandas sympy pyparsing dask dill python-symengine xarray cython cyipopt
echo "!!! pip pycalphad as editable"
pip install -e .
Expand All @@ -60,8 +52,8 @@ script:
conda list
echo "!!! matplotlib py27 fix"
echo 'backend : Agg' > matplotlibrc
echo "!!! running nosetests"
nosetests --with-coverage
echo "!!! running pytest"
pytest --cov=pycalphad
echo "!!! Running ci/deploy script"
bash ci/deploy.sh
coveralls
4 changes: 2 additions & 2 deletions appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@ install:
- "conda config --add channels conda-forge"
- "conda config --add channels pycalphad"
# install pycalphad and dependencies
- "conda install --yes -n condaenv --quiet vc=14 pip setuptools nose numpy pandas scipy sympy pyparsing matplotlib xarray 'tinydb>=3.8' cython cyipopt libpython symengine python-symengine"
- "conda install --yes -n condaenv --quiet vc=14 pip setuptools pytest numpy pandas scipy sympy pyparsing matplotlib xarray 'tinydb>=3.8' cython cyipopt libpython symengine python-symengine"
# Hack to force compiler: https://github.com/pypa/pip/issues/18#issuecomment-73703672
- "pip install --global-option build_ext --global-option --compiler=msvc --editable ."

build: false

test_script:
- "python -c \"import nose ; nose.main()\" -s -v pycalphad"
- "python -c \"import pytest ; pytest.main()\" -s -v pycalphad"
3 changes: 1 addition & 2 deletions conda_recipe/meta.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ requirements:
- setuptools
- matplotlib
- pandas
- nose
- mock
- pytest
- xarray >=0.11.2
- sympy ==1.4
- pyparsing
Expand Down
24 changes: 9 additions & 15 deletions pycalphad/tests/test_calculate.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Model quantities correctly.
"""

import nose.tools
import pytest
from pycalphad import Database, calculate, Model
import numpy as np
try:
Expand All @@ -24,11 +24,10 @@ def test_surface():
calculate(DBF, ['AL', 'CR', 'NI'], 'L12_FCC',
T=1273., mode='numpy')

@nose.tools.raises(AttributeError)
def test_unknown_model_attribute():
"Sampling an unknown model attribute raises exception."
calculate(DBF, ['AL', 'CR', 'NI'], 'L12_FCC',
T=1400.0, output='_fail_')
with pytest.raises(AttributeError):
calculate(DBF, ['AL', 'CR', 'NI'], 'L12_FCC', T=1400.0, output='_fail_')

def test_statevar_upcast():
"Integer state variable values are cast to float."
Expand Down Expand Up @@ -67,31 +66,26 @@ def test_calculate_some_phases_filtered():
calculate(ALFE_DBF, ['AL', 'VA'], ['FCC_A1', 'AL13FE4'], T=1200, P=101325)


@nose.tools.raises(ConditionError)
def test_calculate_raises_with_no_active_phases_passed():
"""Passing inactive phases to calculate() raises a ConditionError."""
# Phase cannot be built without FE
calculate(ALFE_DBF, ['AL', 'VA'], ['AL13FE4'], T=1200, P=101325)
with pytest.raises(ConditionError):
calculate(ALFE_DBF, ['AL', 'VA'], ['AL13FE4'], T=1200, P=101325)


@nose.tools.raises(ValueError)
def test_incompatible_model_instance_raises():
"Calculate raises when an incompatible Model instance built with a different phase is passed."
comps = ['AL', 'CR', 'NI']
phase_name = 'L12_FCC'
mod = Model(DBF, comps, 'LIQUID') # Model instance does not match the phase
calculate(DBF, comps, phase_name, T=1400.0, output='_fail_', model=mod)
with pytest.raises(ValueError):
calculate(DBF, comps, phase_name, T=1400.0, output='_fail_', model=mod)


@nose.tools.raises(ValueError)
def test_single_model_instance_raises():
"Calculate raises when a single Model instance is passed with multiple phases."
comps = ['AL', 'CR', 'NI']
phase_name = 'L12_FCC'
mod = Model(DBF, comps, 'L12_FCC') # Model instance does not match the phase
calculate(DBF, comps, ['LIQUID', 'L12_FCC'], T=1400.0, output='_fail_', model=mod)


if __name__ == '__main__':
import nose
nose.run()
with pytest.raises(ValueError):
calculate(DBF, comps, ['LIQUID', 'L12_FCC'], T=1400.0, output='_fail_', model=mod)
85 changes: 37 additions & 48 deletions pycalphad/tests/test_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
The test_database module contains tests for the Database object.
"""
from __future__ import print_function
import warnings
import pytest
import hashlib
import os
from copy import deepcopy
Expand All @@ -14,15 +14,13 @@
from pycalphad.io.tdb import expand_keyword
from pycalphad.io.tdb import _apply_new_symbol_names, DatabaseExportError
from pycalphad.tests.datasets import ALCRNI_TDB, ALFE_TDB, ALNIPT_TDB, ROSE_TDB, DIFFUSION_TDB
import nose.tools
try:
# Python 2
from StringIO import StringIO
except ImportError:
# Python 3
from io import StringIO

warnings.simplefilter("always", UserWarning) # so we can test warnings

#
# DATABASE LOADING TESTS
Expand Down Expand Up @@ -87,36 +85,28 @@ def test_export_import():
def test_incompatible_db_warns_by_default():
"Symbol names too long for Thermo-Calc warn and write the database as given by default."
test_dbf = Database.from_string(INVALID_TDB_STR, fmt='tdb')
with warnings.catch_warnings(record=True) as w:
with pytest.warns(UserWarning, match='Ignoring that the following function names are beyond the 8 character TDB limit'):
invalid_dbf = test_dbf.to_string(fmt='tdb')
assert len(w) >= 1
expected_string_fragment = 'Ignoring that the following function names are beyond the 8 character TDB limit'
assert any([expected_string_fragment in str(warning.message) for warning in w])
assert test_dbf == Database.from_string(invalid_dbf, fmt='tdb')

@nose.tools.raises(DatabaseExportError)
def test_incompatible_db_raises_error_with_kwarg_raise():
"Symbol names too long for Thermo-Calc raise error on write with kwarg raise."
test_dbf = Database.from_string(INVALID_TDB_STR, fmt='tdb')
test_dbf.to_string(fmt='tdb', if_incompatible='raise')
with pytest.raises(DatabaseExportError):
test_dbf.to_string(fmt='tdb', if_incompatible='raise')

def test_incompatible_db_warns_with_kwarg_warn():
"Symbol names too long for Thermo-Calc warn and write the database as given."
test_dbf = Database.from_string(INVALID_TDB_STR, fmt='tdb')
with warnings.catch_warnings(record=True) as w:
with pytest.warns(UserWarning, match='Ignoring that the following function names are beyond the 8 character TDB limit'):
invalid_dbf = test_dbf.to_string(fmt='tdb', if_incompatible='warn')
assert len(w) >= 1
expected_string_fragment = 'Ignoring that the following function names are beyond the 8 character TDB limit'
assert any([expected_string_fragment in str(warning.message) for warning in w])
assert test_dbf == Database.from_string(invalid_dbf, fmt='tdb')

@pytest.mark.filterwarnings("error")
def test_incompatible_db_ignores_with_kwarg_ignore():
"Symbol names too long for Thermo-Calc are ignored the database written as given."
test_dbf = Database.from_string(INVALID_TDB_STR, fmt='tdb')
with warnings.catch_warnings(record=True) as w:
invalid_dbf = test_dbf.to_string(fmt='tdb', if_incompatible='ignore')
not_expected_string_fragment = 'Ignoring that the following function names are beyond the 8 character TDB limit'
assert all([not_expected_string_fragment not in str(warning.message) for warning in w])
invalid_dbf = test_dbf.to_string(fmt='tdb', if_incompatible='ignore')
assert test_dbf == Database.from_string(invalid_dbf, fmt='tdb')

def test_incompatible_db_mangles_names_with_kwarg_fix():
Expand Down Expand Up @@ -158,33 +148,32 @@ def test_tdb_content_after_line_end_is_neglected():
test_dbf = Database.from_string(tdb_line_ending_str, fmt='tdb')
assert len(test_dbf._parameters) == 3

def _remove_file_with_name_testwritedb():
@pytest.fixture
def _testwritetdb():
fname = 'testwritedb.tdb'
yield fname # run the test
os.remove(fname)

@nose.tools.with_setup(None, _remove_file_with_name_testwritedb)
@nose.tools.raises(FileExistsError)
def test_to_file_defaults_to_raise_if_exists():
def test_to_file_defaults_to_raise_if_exists(_testwritetdb):
"Attempting to use Database.to_file should raise by default if it exists"
fname = 'testwritedb.tdb'
fname = _testwritetdb
test_dbf = Database(ALNIPT_TDB)
test_dbf.to_file(fname) # establish the initial file
test_dbf.to_file(fname) # test if_exists behavior
with pytest.raises(FileExistsError):
test_dbf.to_file(fname) # test if_exists behavior

@nose.tools.with_setup(None, _remove_file_with_name_testwritedb)
@nose.tools.raises(FileExistsError)
def test_to_file_raises_with_bad_if_exists_argument():
def test_to_file_raises_with_bad_if_exists_argument(_testwritetdb):
"Database.to_file should raise if a bad behavior string is passed to if_exists"
fname = 'testwritedb.tdb'
fname = _testwritetdb
test_dbf = Database(ALNIPT_TDB)
test_dbf.to_file(fname) # establish the initial file
test_dbf.to_file(fname, if_exists='TEST_BAD_ARGUMENT') # test if_exists behavior
with pytest.raises(FileExistsError):
test_dbf.to_file(fname, if_exists='TEST_BAD_ARGUMENT') # test if_exists behavior

@nose.tools.with_setup(None, _remove_file_with_name_testwritedb)
def test_to_file_overwrites_with_if_exists_argument():
import time
def test_to_file_overwrites_with_if_exists_argument(_testwritetdb):
"Database.to_file should overwrite if 'overwrite' is passed to if_exists"
fname = 'testwritedb.tdb'
import time
fname = _testwritetdb
test_dbf = Database(ALNIPT_TDB)
test_dbf.to_file(fname) # establish the initial file
inital_modification_time = os.path.getmtime(fname)
Expand All @@ -193,20 +182,20 @@ def test_to_file_overwrites_with_if_exists_argument():
overwrite_modification_time = os.path.getmtime(fname)
assert overwrite_modification_time > inital_modification_time

@nose.tools.raises(ValueError)
def test_unspecified_format_from_string():
"from_string: Unspecified string format raises ValueError."
Database.from_string(ALCRNI_TDB)
with pytest.raises(ValueError):
Database.from_string(ALCRNI_TDB)

@nose.tools.raises(NotImplementedError)
def test_unknown_format_from_string():
"from_string: Unknown import string format raises NotImplementedError."
Database.from_string(ALCRNI_TDB, fmt='_fail_')
with pytest.raises(NotImplementedError):
Database.from_string(ALCRNI_TDB, fmt='_fail_')

@nose.tools.raises(NotImplementedError)
def test_unknown_format_to_string():
"to_string: Unknown export file format raises NotImplementedError."
REFERENCE_DBF.to_string(fmt='_fail_')
with pytest.raises(NotImplementedError):
REFERENCE_DBF.to_string(fmt='_fail_')

def test_load_from_stringio():
"Test database loading from a file-like object."
Expand All @@ -218,25 +207,25 @@ def test_load_from_stringio_from_file():
test_tdb = Database.from_file(StringIO(ALCRNI_TDB), fmt='tdb')
assert test_tdb == REFERENCE_DBF

@nose.tools.raises(ValueError)
def test_unspecified_format_from_file():
"from_file: Unspecified format for file descriptor raises ValueError."
Database.from_file(StringIO(ALCRNI_TDB))
with pytest.raises(ValueError):
Database.from_file(StringIO(ALCRNI_TDB))

@nose.tools.raises(ValueError)
def test_unspecified_format_to_file():
"to_file: Unspecified format for file descriptor raises ValueError."
REFERENCE_DBF.to_file(StringIO())
with pytest.raises(ValueError):
REFERENCE_DBF.to_file(StringIO())

@nose.tools.raises(NotImplementedError)
def test_unknown_format_from_file():
"from_string: Unknown import file format raises NotImplementedError."
Database.from_string(ALCRNI_TDB, fmt='_fail_')
with pytest.raises(NotImplementedError):
Database.from_string(ALCRNI_TDB, fmt='_fail_')

@nose.tools.raises(NotImplementedError)
def test_unknown_format_to_file():
"to_file: Unknown export file format raises NotImplementedError."
REFERENCE_DBF.to_file(StringIO(), fmt='_fail_')
with pytest.raises(NotImplementedError):
REFERENCE_DBF.to_file(StringIO(), fmt='_fail_')

def test_expand_keyword():
"expand_keyword expands command abbreviations."
Expand Down Expand Up @@ -402,12 +391,12 @@ def test_species_are_parsed_in_tdb_phases_and_parameters():
assert test_dbf_reread.phases['T2SL'].constituents == ({Species('AL+3')}, {Species('O-2')})
assert len(test_dbf_reread._parameters.search(where('constituent_array') == ((Species('AL+3'),),(Species('O-2'),)))) == 1

@nose.tools.raises(ParseException)
def test_tdb_missing_terminator_element():
tdb_str = """$ Note missing '!' in next line
ELEMENT ZR BCT_A5
FUNCTION EMBCCTI 298.15 -39.72; 6000 N !"""
Database(tdb_str)
with pytest.raises(ParseException):
Database(tdb_str)


def test_database_parsing_of_floats_with_no_values_after_decimal():
Expand Down
13 changes: 6 additions & 7 deletions pycalphad/tests/test_energy.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,15 @@
correct abstract syntax tree for the energy.
"""

import nose.tools
import pytest
from sympy import S
from pycalphad import Database, Model, calculate, ReferenceState
from pycalphad import Database, Model, ReferenceState
from pycalphad.core.utils import make_callable
from pycalphad.tests.datasets import ALCRNI_TDB, FEMN_TDB, ALFE_TDB, \
CRFE_BCC_MAGNETIC_TDB, VA_INTERACTION_TDB, CUMG_TDB
from pycalphad.core.errors import DofError
import pycalphad.variables as v
import numpy as np
import warnings

DBF = Database(ALCRNI_TDB)
ALFE_DBF = Database(ALFE_TDB)
Expand All @@ -21,12 +20,12 @@
CUMG_DBF = Database(CUMG_TDB)
VA_INTERACTION_DBF = Database(VA_INTERACTION_TDB)

@nose.tools.raises(ValueError)
def test_sympify_safety():
"Parsing malformed strings throws exceptions instead of executing code."
from pycalphad.io.tdb import _sympify_string
teststr = "().__class__.__base__.__subclasses__()[216]('ls')"
_sympify_string(teststr) # should throw ParseException
with pytest.raises(ValueError):
_sympify_string(teststr)


def calculate_output(model, variables, output, mode='sympy'):
Expand Down Expand Up @@ -234,12 +233,12 @@ def test_reference_energy_of_unary_twostate_einstein_magnetic_is_zero():
check_output(m, statevars, 'GMR', 0.0)


@nose.tools.raises(DofError)
def test_underspecified_refstate_raises():
"""A Model cannot be shifted to a new reference state unless references for all pure elements are specified."""
m = Model(FEMN_DBF, ['FE', 'MN', 'VA'], 'LIQUID')
refstates = [ReferenceState(v.Species('FE'), 'LIQUID')]
m.shift_reference_state(refstates, FEMN_DBF)
with pytest.raises(DofError):
m.shift_reference_state(refstates, FEMN_DBF)


def test_reference_energy_of_binary_twostate_einstein_is_zero():
Expand Down
Loading

0 comments on commit 74d8917

Please sign in to comment.