Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Testing #31

Merged
merged 1 commit into from
May 25, 2018
Merged

[WIP] Testing #31

merged 1 commit into from
May 25, 2018

Conversation

tkhyn
Copy link
Contributor

@tkhyn tkhyn commented Apr 5, 2018

Following #5, this is a WIP regarding adding tests to exhale.

The main idea is to use a mock sphinx application, which is a plain old object with corresponding attributes and with the configuration exhale needs for its setup.

More info in comments below, as the structure of the testing package may evolve.

@tkhyn
Copy link
Contributor Author

tkhyn commented Apr 5, 2018

  1. How to run the tests:
cd /path/to/exhale/package
pip install zc.buildout
buildout
bin/tests

For those who don't know what buildout is, it's 'just' a far superior alternative to virtual environments. I recommend setting up a ~/.buildout/default.cfg file containing this to make the most of it (= all packages are installed in the same directory).

If you want coverage information, just use:

bin/tests --cov
  1. Quick overview of the testing package
/testing/                       # the testing package
  
  /test_projects/               # sample projects
    /c_maths/                   
      /doc/                     # the documentation directory, may not exist prior
        /_doxy                  # doxygen's output directory
        /api                    # exhale's output directory        
      /*/                       # any directory containing source files to parse
    
    /[language]_[name]/         # another sample project
   
  
  /tests/                       # tests are here
    /c_maths.py                 # tests relevant to the c_maths project
   
  /base.py                      # contains ExhaleTestCase, the base class for exhale tests
                                # may be renamed to testcases.py
  /mocks.py                     # contains SphinxAppMock, to ... mock a sphinx app!
  /utils.py                     # utility functions needed for tests setup
  1. Writing a basic unit test:

Don't worry about ExhaleTestCase, just create a file in the testing/tests directory, import whatever module / class / function you need to test and derive a class from unittest.Testcase.

  1. Writing a 'full-blown' test:

Just check out testing/tests/c_maths.py for an example! A few things to know:

  • you need to specify a test_project that you'll test against
  • you may want to specify a config to override the default configuration (the way it works will certainly evolve in the near future)
  • ExhaleTestCase.configure() configures exhale according to the provided configuration
  • ExhaleTestCase.run_exhale() simply ... runs exhale. Generated output files can then be compared to what should be expected.

@tkhyn
Copy link
Contributor Author

tkhyn commented Apr 5, 2018

Warning for Windows users: it won't work for you :). See #30 for patch suggestions, and hopefully some fixes will be merged soon!

@svenevs
Copy link
Owner

svenevs commented Apr 6, 2018

THIS IS EPIC! I'm quite excited to jump in on this, right now I plan to commit saturday for fixing this stuff. The above explanation and the need of specifying the doxygen output may hint at the underlying problem with Windows. When running Doxygen I'm explicitly changing working directories which may be what is breaking on windows? Expect more detailed feedback tomorrow, thanks again for putting so much time into this I really appreciate it!!!

Assuming Windows stuff gets fixed, then tests will run on Windows right? AKA there's nothing about zc.buildout that would be a problem here, it's just that Exhale is broken on Windows at this time.

@tkhyn
Copy link
Contributor Author

tkhyn commented Apr 6, 2018

there's nothing about zc.buildout that would be a problem here, it's just that Exhale is broken on Windows at this time

Yes, zc.buildout is just a tool used to generate an isolated virtual environment to run the tests. Once the script is generated it has no influence whatsoever on how the tests run.

Today I've tried to tweak the tests to run with Python 2.7 (I'd like to add a tox config with python 3.6 and 2.7), and I ran into an issue: the fact you're defining __name__ in the configs module prevents reload from working (for testing it is necessary to reload the configs module) on python 2.7. Is it really needed anywhere? Commenting out the __name__ = 'configs' line enable the tests to work.

@svenevs
Copy link
Owner

svenevs commented Apr 6, 2018

I just got travis working with the tests. I'm gonna get more integrations setup on that sandbox repo and then just add them here. This branch should automatically start firing once i setup CI here.

For the buildout config python version, is that necessary?

@tkhyn
Copy link
Contributor Author

tkhyn commented Apr 6, 2018

For the buildout config python version, is that necessary?

It's not really necessary but I usually find it quite useful for quick testing. Fire bin/python and you have an interpreter with all the packages only one import statement away. Usually all buildout configs have at least one intepreter for that reason.

Tests on the c_maths project
"""

from testing.base import ExhaleTestCase
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So for python 2 it's unable to import testing.base. I tried to import exhale just above it but the CI logs are inaccessible right now. I'm leaning toward ditching buildout, if I can't test py2 it's a no-go (since many people still use that on RTD).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it has nothing to see with buildout really. buildout does not care if you're using python 2 or 3, it just generates a script and sets sys.path according to the packages it's told to include.

Copy link
Contributor Author

@tkhyn tkhyn Apr 7, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

afterthought: did you add the missing __init__.py files before testing with python 2?? Because without them python 2 is lost. (add one in the testing and one in testing/tests)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah i added some __init__.py and various other things, the python2 importing now works!

testing/utils.py Outdated


@contextmanager
def cd(path):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of this fancy stuff came from you right? Very swanky

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it must have come from somewhere in the first place, but it ended up in my toolbox as it's a very useful piece of code :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haha, just realised it was used in sphinx as well: from sphinx.util.osutil import cd

testing/utils.py Outdated
os.chdir(old_dir)


def deep_update(orig, override):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the motivation for this method is because the configs are stored at the class-level?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would just be used to update a default configuration with a custom one. If the configuration values fitted in a simple dict, we could just use dict.update, but here we have one more level (with the exhale_args attribute).

It was just a quick way to do it, as I had this deep_update function in my toolbox, so feel free to change the way it's done if you find a better / clearer way!

@svenevs
Copy link
Owner

svenevs commented Apr 7, 2018

Ok so in addition to buildout troubles with python 2 on travis and importing issues, AppVeyor using buildout fails to install lxml whereas vanilla pip will work just fine. I'm gonna call it a night but I'll be committing the modifications to this branch at some point tomorrow. The only integrations left to solve are the OSX CI and Coverage.

I can say with absolute certainty that this would not have happened this quickly without your epic kickstart. Thank you again so much!

P.S. I really like the idea of having C and CPP tests. That didn't even occur to me :)

@tkhyn
Copy link
Contributor Author

tkhyn commented Apr 7, 2018

Regarding your issues with AppVeyor I would venture into saying that setuptools is not up-to-date. Prior to version 38.x it indeed failed to install wheel packages as buildout eggs (typically lxml)

I have no problem if you want to ditch buildout, I just think it's a great tool that can help save time (and virtual environment space !) provided you get over the slightly unwelcoming issues. I use it for many things: run tests, generate documentation, create management or server scripts for Django apps, etc ...

If you are interested in building Python apps with C/C++ modules, including testing and documentation in both languages, I have just put together a template project that should work out of the box (and that's the reason I got interested in exhale), you're welcome to give it a go and report any issue or suggest features: https://bitbucket.org/tkhyn/py_cpp.

@svenevs
Copy link
Owner

svenevs commented Apr 7, 2018

I'm looking a little more closely at what's up with buildout. It's strange because I can install lxml with pip on appveyor and things are good, but with buildout something messes up. I'm giving this recipe a shot but honestly even if it works I'll want to do something else. The libxml2 FTP server is very very slow.

@tkhyn
Copy link
Contributor Author

tkhyn commented Apr 8, 2018

Do you mind sharing the traceback of the issue you're getting regarding lxml installation on AppVeyor? And or the result of easy_install --version?

@svenevs
Copy link
Owner

svenevs commented Apr 8, 2018

https://gist.github.com/svenevs/4b8de3a9c0561053ac66e1ba9242f8c8

buildout using easy_install means no pre-compiled binaries (as far as I understand it). The build failure is coming from a lack of libxml2, which is too painful to get around on AppVeyor. pip on the other hand, can install the lxml pre-compiled binaries no problem. If I had to guess, I'd say the buildout people are assuming that if you want something like libxml2 you would create a recipe for it rather than relying on pre-compiled binaries. Doing that takes forever on AppVeyor though.

P.S. I've got quite a few local changes, there's like 3-4 official (but different) ways to test a sphinx app. I'm still reading through the sphinx dev-list threads but it seems people prefer nose for this. I've got one thing working, but integrating it into metclasses is problematic. The good news is I think I may have found the root Windows problem (untested, but hypothesis) when getting the full sphinx test app in there :)

@tkhyn
Copy link
Contributor Author

tkhyn commented Apr 8, 2018

I'm still reading through the sphinx dev-list threads but it seems people prefer nose for this

nose or py.test, honestly, does not change much and it's quite easy to swap from one to the other. My experience is that py.test is more flexible and is easier to tweak when you need to do something fancy at a later stage...

@tkhyn
Copy link
Contributor Author

tkhyn commented Apr 8, 2018

buildout using easy_install means no pre-compiled binaries (as far as I understand it)

No. buildout can install binaries. Installing lxml 4.2.1 works fine on windows provided setuptools 39.0.1 is available in the environment (it downloads lxml-4.2.1-cp27-cp27m-win_amd64.whl and installs it as pip would do it) ... easy_install --version is just a way to know the version of setuptools, which internals are used by buildout.

The error you're seeing here (thanks for sharing the gist) typically happens when the version of setuptools is older than 38.x.

@svenevs
Copy link
Owner

svenevs commented Apr 8, 2018

does not change much and it's quite easy to swap from one to the other

Ok good to know 👍 So there's this way of doing things, what are your thoughts?

import pytest
pytest_plugins = 'sphinx.testing.fixtures'

# test_docs_dir is path to a conf.py that contains breathe_projects, exhale_args etc
@pytest.mark.sphinx('html', testroot=test_docs_dir)
def test_project(app):# should be able to have (self, app): if in class
    containmentFolder    = app.config.exhale_args["containmentFolder"]
    containmentFolder    = os.path.abspath(os.path.join(app.srcdir, containmentFolder))
    rootFileName         = app.config.exhale_args["rootFileName"]
    rootFileTitle        = app.config.exhale_args["rootFileTitle"]
    doxygenStripFromPath = app.config.exhale_args["doxygenStripFromPath"]
    assert os.path.isfile(os.path.join(containmentFolder, rootFileName))

The way this works is there needs to be a conf.py (which could be generated, and index.rst too). Sphinx is looking for these. What I want to do is

class ProjectTestCaseMetaclass(type):
    def __new__(cls, name, bases, attrs):
        # check for none and some stuff
        # set some class level variables using existing ones

class ProjectTestCase(with_metaclass(ProjectTestCaseMetaclass, unittest.TestCase)):
    test_project  = None
    test_root     = None # set in metaclass
    test_docs_dir = None # set in metaclass
    test_stamp    = None # set in metaclass

    # vvv what I want to do but is not working
    @pytest.mark.sphinx('html', testroot=test_docs_dir)
    def test_project(app):# should be able to have (self, app): if in class
        # test some common things like library_root.rst existing
        # note: app.build() not necessary, exhale is already finished by this point

The idea being with e.g. test_project = "c_maths" test_root would be set to projects/c_maths/, the conf.py could be generated to projects/c_maths/docs/conf.py etc. So then in tests/c_maths.py there's really just a class

class CMathsProject(ProjectTestCase):
    test_project = "c_maths"
    # could also just set conf.py string or override values here too but let the metaclass generate it

I don't know if all of that is possible, i need to sleep on it.

Installing lxml 4.2.1 works fine on windows provided setuptools 39.0.1 is available in the environment

Ahhh. All I needed was to pip install -U setuptools NICE. I've never been so happy to see a testing error (from the actual tests). This is great, I really like the concept of buildout especially for local testing convenience for developers. I don't have to worry about a virtualenv setup and activate, it's just done!

@tkhyn
Copy link
Contributor Author

tkhyn commented Apr 8, 2018

So there's this way of doing things, what are your thoughts?

I've nothing wrong with that at all, despite it not being the approach I chose.

My view was to actually try to avoid launching the full sphinx machinery in the tests as the only entry points in exhale for sphinx are configs.apply_sphinx_configurations and deploy.explode, which can be tested independently of sphinx itself, and we don't actually need to test the full html, pdf, latex, etc. output of sphinx, just at most check the *.rst generated by exhale, and no further.

Basically I was assuming that sphinx is sufficiently tested to:

  • correctly load a conf.py file
  • correctly associate event handlers (environment_ready here) to events (basically anything that happens before exhale kicks in)
  • correctly generate any desired output from RST files (basically anything that happens after exhale has finished its job)

and that these points are none of exhale's business and it is not necessary to actually fully load sphinx and make it generate HTML documents. It's ok as there are only 2 tests right now, but once you get more the overhead can become significant. The focus should be on anything actually belonging to exhale (namely what happens in environment_ready)

The only real downside of the 'mock' approach I chose is that the tests will still pass in case sphinx changes its API and the way the configuration is stored. However that's not very likely to happen and it would be a breaking change that could easily be managed.

One reason it could be useful to have the actual sphinx app at hand in the future is if features (for example using more event handlers than one or two) are added. But I'm not sure how likely it is to happen.

I don't have any hard opinion on that really, it's just that my approach is to simplify and optimize things whenever I can, and actually run tests only on the things that actually need testing / aren't tested elsewhere to avoid introducing issues that are not mine/ours! It's basically about decoupling, and removing as many unknowns or variables as you can from an equation before solving it :). Same thing regarding a conf.py. I thought 'why bother generating / using a python module when we can use a dictionary to populate mock objects?'. It's a lot faster, cleaner, simpler and easier to debug, and is unlikely to introduce any issue.

So, in the end, I'd say it's your call! Either you want to launch sphinx which will take time to do heaps of things to setup a test (open and load a conf.py, set plenty of parameters that exhale does not care about, and possibly make it output a bunch of html or pdf files that are not needed at all to test exhale's features just to have a builder-inited event sent) or simply quickly generate a basic mock sphinx app with only the parameters needed to test what's written on exhale's package!

As a side note, I have some input on the way the exhale parameters are extracted from the sphinx app, as it affects testing. There may have been a good reason to use a module (the configs module) to do so, but in my view it adds extra complexity (e.g. setting global variables) and requires using tricks (such as reloading a module!) when testing. If a singleton class instance to store the configuration was used instead of a module, things would be much clearer, cleaner and easier to understand.

Glad you made it work with buildout. Similar to you it brought me a bit of frustration in the beginning but now saves me so much time and hassle I could hardly do without it.

(and sorry for the wall of text! did not have the courage to condense it more once written!)

@svenevs
Copy link
Owner

svenevs commented Apr 8, 2018

The issue is that the SphinxMockApp does not set things that Exhale relies on, primarily app.srcdir and app.confdir (confusion above when I thought it was a full sphinx app). It would definitely be easy to set these, but going the extra mile and relying on the state that sphinx sets is worth it. To that end, I think I've finally found the way to do this. The confusion is because the new way of testing sphinx extensions is a result of a couple of things internally from sphinx, but in 1.6+ they exposed their own testing stuff to developers. Interestingly, I can skip even running the builder, so no html etc is generated unless I want to. The real motivation is that it means the windows tests are not going to be false positives if I set app.srcdir and app.confdir to what I want them to be xD If it doesn't pan out in a couple hours though, I'll just mark it as a TODO and set them manually for the one you made. Clever trick about checking __module__ for allowing the primary parent test class to have e.g. test_project = None btw!

As far as the module level configs thing....god yes. One of the python 2 problems btw was you need to from six.moves import reload_module hehe. That's all going to get wrapped into a configs.Config object, which will also be required for multi-project stuff. I knew when I wrote it I was going to regret doing it at module scope. Now I reallllly regret it! Windows is priority 1 though, because that's going to require big changes. My one saving grace: I avoided from exhale.configs import *, and always did things like configs.containmentFolder, so hopefully it won't be hard so much as tedious!

But yes in the end, not all test cases are going to be project based....because those all require a full execution with doxygen and the like.

@tkhyn
Copy link
Contributor Author

tkhyn commented Apr 8, 2018

Ok, got the point about srcdir and confdir that are set by sphinx. That indeed makes perfect sense, generating false positives is not the thing we're aiming at! And if it's possible avoiding running the builder then one of my main concerns disappear so I'm more than happy to get rid of the mock app and to suse your template code above as a basis.

The only thing I'd be looking that, then, is how to pass custom configuration to the sphinx app pytest fixture in a very simple way (i.e. without having to create a conf.py by hand for each test case, which would make it hard to read and maintain, as the configuration would be away from testcases). Ideally I'd want to find a way to not generate a conf.py an simply pass any configuration option directly to the sphinx app but I'm not sure it's actually possible. Maybe by overriding the app pytest fixture ... I'm happy to have a look at that. Did you have a go already? Or would you be able to publish your work on the testing branch so that we don't duplicate our efforts?

I don't think the refactoring of the configs module will be such a big deal. Indeed it's not top priority, but will be needed at some stage. (btw on python 2.7 reload is built-in, and is also part of imp, which is deprecated ...)

@svenevs
Copy link
Owner

svenevs commented Apr 8, 2018

Yeah gimme a couple of minutes to redo the stuff that works. I've been working in a different repo because I knew getting the CI stuff setup was going to require a large number of commits. I'll just make a self contained file here with how their fixture stuff works, and then post back with where I think it needs to go.

@tkhyn
Copy link
Contributor Author

tkhyn commented Apr 8, 2018

Cool, thanks.

Just had a look, there is a very promising confoverrides kwarg in sphinx.testing.util.SphinxTestApp's __init__. :)

@svenevs
Copy link
Owner

svenevs commented Apr 8, 2018

Ok I'm not sure how I push to this branch, see this commit on the unintended pr/31 branch...

So yeah the problem is that I can't get @pytest.mark.sphinx to work like this in the metaclass new, e.g.

def __new__(cls, name, bases, attrs):
    # check things that need to be set in children...
    # set test_docs_dir based on test_project name indexing into projects/{test_project_name}/docs
    @pytest.mark.sphinx('html', testroot=test_docs_dir)
    def project(app):
        # run some common tests

    attrs["test_exhale_project"] = project

You can't bind it, e.g. if you add a self in there before app. I don't think you can do the fixtures / marking this way. So I was looking instead at

https://github.com/sphinx-doc/sphinx/blob/6030e7390df37f0e3cd25cdc1e56dbb95107c69d/sphinx/testing/fixtures.py#L143

I think the make_app method is what we want, or at least those docs make it seem like you aren't supposed to create a SphinxTestApp explicitly. Down the line the args get passed through, as you noticed with things like confoverrides available (SWEET). Right now I'm just assuming there's a conf.py and index.rst in the projects' name docs/ dir.

But at this point I can't figure out how to use make_app, it yields a generator. They have some "examples" of it, but their make_app is callable somehow.

I will figure out how to commit this here now because the changes to exhale/configs.py need to happen

@svenevs
Copy link
Owner

svenevs commented Apr 8, 2018

@tkhyn you should be able to git pull now, i thought I could work in the same repo and change my remotes but clearly did that wrong. Anyway the working alternate with the fixture is in there, I'm trying to figure out make_app, but maybe it's just easier to create an explicit SphinxTestApp regardless of their note saying not to do so...

@tkhyn
Copy link
Contributor Author

tkhyn commented Apr 8, 2018

Great, I'll have a closer look very soon

kwargs to SphinxTestApp can be passed using @pytest.mark.sphinx(....) exactly like you pass testroot. E.g:

@pytest.mark.sphinx('html', testroot=test_docs_dir, confoverrides={'whatever': 'we_want'})

The way to apply a decorator to a method in the metaclass could be something like (warning, untested code, may be flawed!!):

def __new__(cls, name, bases, attrs):
    def project(self, app):
         # whatever you want project() do do
    attrs['project'] = project
    c = super(ExhaleTestCaseMetaclass, cls).__new__(cls, name, bases, attrs)
    pytest.mark.sphinx('html', testroot=test_docs_dir, confoverrides={'whatever': 'we_want'})(c.project)
    return c

The reason they are using generators is there is some setup and teardown / cleanup code. Anything above the yield statement is executed before the test. Anything under it is executed after it.

@svenevs
Copy link
Owner

svenevs commented Apr 9, 2018

OH SNAP. Yeah that looks like something that would work, instead of my nonsense...cool I'll take a look after dinner, I think I should be able to finalize this stuff tonight!

@tkhyn
Copy link
Contributor Author

tkhyn commented Apr 9, 2018

yeah I'm not 100% sure about when the decorator should be called (i.e. before the super call or after it). Metaclass stuff can be a bit tricky ...

So it looks like we could have a unique generic conf.py somewhere and simply apply config overrides for every test in a test class to set breathe and exhale config according to what the tests are about. Looks good!

@svenevs
Copy link
Owner

svenevs commented Apr 9, 2018

Gah whatever this is a waste of time, the decorators are nice but I cannot for the life of me figure out how to bind this thing. There's some discussion online about types.MethodType, but in addition to decorating and the decorator being a pytest.fixture something's up and I don't know enough to fix it.

# necessary imports
from sphinx.testing.path import path
from sphinx.testing.util import SphinxTestApp

# in the metaclass
def setup_project(self):
    # inspired by cd, just creates an empty conf.py
    # the rest is handled via confoverrides :)
    with conf_py(test_docs_dir):
        app = SphinxTestApp(
            buildername="html",
            srcdir=path(test_docs_dir),
            freshenv=False,
            confoverrides=confoverrides,
            status=None,
            warning=None,
            tags=None,
            docutilsconf=None
        )
        assert isinstance(app.config.exhale_args, dict)
        print("You did it, yay!")

The only concern is what you mentioned earlier about setup / cleanup. Anything with respect to cleanup come to mind?

@tkhyn
Copy link
Contributor Author

tkhyn commented Apr 9, 2018

All right, got it working. Check out the commit above (python 2 compatibility will come tomorrow in another commit).

flake8-docstrings
flake8-import-order
pep8-naming
flake8-colors
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure i want all of the errors these are spitting out to fail, some of them I fundamentally disagree with.

docs/testing.rst Outdated
.. todo::

Finalize these docs.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tkhyn any chance you would be willing to fill in these docs, describing what :class:pytest.fixture you have enabled and what they do?

I also want to change ExhaleTestCase::config to be confoverrides for consistency, but if you're willing to write these docs maybe you could change that too? WARNING: see hack for docs in testing/base.py, there are unfortunately two class definitions. One for the docs and one for the tests (otherwise the ExhaleTestCase docs were doing something really strange where it just said "alias of ExhaleTestCase" which linked to itself.

Locally I'm working on two unrelated components of the test framework

  1. Validation of the hierarchy (almost finished).
  2. Finalizing the CI stuff.

The docs for all the testing don't have to be perfect, I mostly just need them for myself so that after merging this I can peel off and make new project test cases as time permits.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P.S. I am aware of the docs test failing right now, I know what the issue is but am awaiting a response before fixing.

@svenevs
Copy link
Owner

svenevs commented Apr 12, 2018

the 1st parameter is actually the metaclass itself

corrected. I should have known better...

sphinx removes it in the documentation

it doesn't for me, nor would i like it to.

It looks like you've found a bug in Python !!

to be honest, the things you are doing here are beyond my knowledge of the internals of python. for example, in languages like Java or C++ you should do the super part first through the base constructor. but i don't think that's needed in python, most examples I see of __new__ etc do it last.

why is this a python bug and not something to do with sphinx (or autodoc)? the tests ran fine previously, it's just the documentation that was broken

@tkhyn
Copy link
Contributor Author

tkhyn commented Apr 12, 2018

corrected. I should have known better...

no problem, I agree it's quite confusing!

it doesn't for me, nor would i like it to.

it removes it in the signature (it shows __new__(name, bases, attrs))

why is this a python bug and not something to do with sphinx (or autodoc)? the tests ran fine previously, it's just the documentation that was broken

I tracked it down by debugging deep into sphinx code and realised sphinx had nothing to do with it. I've added minimal scripts to expose what happens in the bug report. Ok, that was not a python bug actually, just a bad loop variable name!!

@tkhyn
Copy link
Contributor Author

tkhyn commented Apr 12, 2018

And yes, I'll definitely attempt to come up with something understandable in the docs regarding the magic surrounding ExhaleTestCase and pytest.fixtures.

@svenevs
Copy link
Owner

svenevs commented Apr 12, 2018

hehehe well i didn't see it either. thank you for fixing it proper :)

thank you very much for being willing to document it too! I respect and fear metaclasses and decorators. i have a surface level understanding of them, but as witnessed by the super cool stuff you've added here i know which part of python i want to focus on learning more intimately next

@tkhyn
Copy link
Contributor Author

tkhyn commented Apr 12, 2018

no worries

As I'm writing the docs on ExhaleTestCase's fancy variables and decorators, I'm contemplating ditching the config class-level variable - the one you wanted to rename to confoverrides - and turn the @with_config decorator into a @confoverrides decorator that can be applied to test class or a test method. That's likely to change how the pytest fixtures are applied, and therefore require a bit of work, but I think it will be much clearer than having a variable and a decorator basically doing the same thing. Your thoughts?

@svenevs
Copy link
Owner

svenevs commented Apr 13, 2018

In light of recent discussions, I will fix the singular docs consistency thing. My local changes are going to be a little challenging to merge into this, so please hold off committing until they are up here (I'll greenlight you again, but I think we're almost done with this!).

Out of curiosity, what time zone are you in?

testing/base.py Outdated
############################################################################
# Automatically create docs/{conf.py,index.rst} for the test project. #
############################################################################
def _with_docs_dir(self):
Copy link
Owner

@svenevs svenevs Apr 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously before return default_confoverrides I was setting a class-level cls.config variable (hence the question earlier). Locally, with either scope="function" or scope="class" the testing/projects/{test_project}/docs/{conf.py,index.rst} are currently being created, but not on Travis for some reason. Locally they are not deleted though.

I had done cls = default_confoverrides and bound _with_docs_dir later because without it self.app.config is not available. So it seems there is a more glaring problem that I will investigate further.

  • Glaring problem: @confoverrides changing containmentFolder or rootFileName will break that, hence the strict requirements on ExhaleTestCase.copyThisProject. I don't think an elegant solution is needed, because the would-be test-case rendering site is going to assume that {containmentFolder} := test_project and {rootFileTitle} := {test_project}_root.rst (as changed in make_default_config).

The idea is in the future I will want to be able to actually save all of the generated reStructuredText and build out the testing website. I'm probably going to do it on a gh-pages branch (far far off in the future) because it will be very useful to actually set GENERATE_HTML = YES to have the ground truth (Doxygen) included.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way to change the sphinx root directory for the sphinx.testing.fixtures.app is to use pytest.mark.parametrize. Some digging will be needed to know where and with which arguments to call it.


# If a given test case needs to run app.build(), make sure index.rst
# is available as well
cls_exhale_args = sphinx_kwargs["confoverrides"]["exhale_args"]
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tkhyn Ok so this is where my understanding breaks.

  1. The goal is automatically creating testing/projects/{test_project}/docs/{conf.py,index.rst}.
    • Currently this is done with scope=class, which is a problem when containmentFolder or rootFileName are changed in conf.py.
    • Not a huge deal (I will document it more explicitly, see testing.base.ExhaleTestCase.copyThisProject)?
  2. I don't understand what's going on with the priority based @pytest.mark.exhale and the sphinx_kwargs. I've read the marking docs like three times, but don't understand where this priority actually comes from.

Basically, what are your thoughts on this? The reason for switching to project local docs/conf.py is because if you run the tests in parallel with a single TEST_DOCS_DIR/conf.py things can break. Creating it at the function scope may not solve this either though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say the way to change the sphinx root directory used by sphinx.testing.fixtures.app is to use pytest.mark.parametrize for the "app_params" fixture (see sphinx.testing.fixtures). Some digging will be needed to know where and with which arguments to call it.

testing/base.py Outdated
The following are assumed:

1. Every derived class of :class:`testing.base.ExhaleTestCase` calls this
method **exactly once**.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'exactly once' or 'at most once'? and actually why? If you call it twice it'll just override the files in the test results dir...
There are ways to enforce it by setting / checking a _project_copied attribute:

if hasattr(self, '_project_copied'):
    return
[...]
self._project_copied = True

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So yeah I think I want to just remove this stuff for now, as a proof of concept it works fine but having learned more about marking I think the better approach is to define test methods on a subclass of ExhaleTestCase and mark them explicitly. Since this was all just a fantasy for what is to come in due time, it's better to just remove it for now.

This stuff will become useful if / when I decide to battle the template issues again. That's a long story though, and I've yet to decide if I actually want to go down that road (again).

testing/base.py Outdated

For (2), it is really just important that the path to
``{containmentFolder}/{rootFileName}`` is the same as what is specified
by :func:`testing.base.make_default_config`.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, I am quite confident there are ways to enforce and/or mitigate this.
I feel it's overkill to prevent the use of @confoverrides just because it may change one value.

testing/base.py Outdated
# otherwise we need a ``test_project`` attribute
raise RuntimeError('ExhaleTestCase subclasses must define a "test_project" attribute')

# Run all test projects local to the project folder
testroot = os.path.join(TEST_PROJECTS_ROOT, test_project, "docs")
attrs["testroot"] = testroot
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you really think you're going to need parallel testing, then you need a per-test testroot. As tests for the same project will overwrite each other

testing/base.py Outdated
############################################################################
# Automatically create docs/{conf.py,index.rst} for the test project. #
############################################################################
def _with_docs_dir(self):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way to change the sphinx root directory for the sphinx.testing.fixtures.app is to use pytest.mark.parametrize. Some digging will be needed to know where and with which arguments to call it.

tox.ini Outdated
# deps =
# {[testenv]deps}
# {[testenv]deps_py2}
# TODO [testenv:distribute] <- streamline bulding pypi releases
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so you really want to ditch python 2.7 / 3.x testing? Why?
In my opinion it's quite important to make sure that the test suite completes on all target version of python


# If a given test case needs to run app.build(), make sure index.rst
# is available as well
cls_exhale_args = sphinx_kwargs["confoverrides"]["exhale_args"]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say the way to change the sphinx root directory used by sphinx.testing.fixtures.app is to use pytest.mark.parametrize for the "app_params" fixture (see sphinx.testing.fixtures). Some digging will be needed to know where and with which arguments to call it.

################################################################################
# Automatically create docs/{conf.py,index.rst} for the test project. #
################################################################################
def _with_docs_dir(self):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd think it would be better to leave this code near the _set_app definition as it's a test case setup thing rather than closely related to how the configuration is determined.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I've solved it once and for all! In conftest.py you can define these two key methods

def pytest_runtest_setup(item):
def pytest_runtest_teardown(item, nextitem):

With some additional class level attribute saving and extra machinery it appears to all be working as expected. It got pretty ugly, so I'm going to clean that up tomorrow and then push ;)

@tkhyn
Copy link
Contributor Author

tkhyn commented Apr 14, 2018

Out of curiosity, what time zone are you in?

KiwiZone :) Good evening!

@svenevs
Copy link
Owner

svenevs commented Apr 14, 2018

KiwiZone :) Good evening!

Too right! I'm in UTC-7, I figured you had to be somewhere else since normally when I'm working on this I'm the only one up.

Thanks for the feedback, I'll do some digging right now!

"""
# If not explicitly overriden here, somehow the `func_to_sphinx_map` is getting
# populated with previous cls entries.
cls.func_to_sphinx_map = {}
Copy link
Owner

@svenevs svenevs Apr 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tkhyn what do you think of the new strategy (see testing/conftest.py's setup and teardown methods). The only thing I didn't understand is why I had to reset to {} for every time this method was called, but as far as I understand this is exactly what should be done.

The reason it was confusing was because the test methods defined in a separate class CMathsTestsNoRun were somehow getting in here if I don't reset... (e.g. set a trace in conftest.py/pytest_runtest_setup and look at item.cls.func_to_sphinx_map).

Copy link
Contributor Author

@tkhyn tkhyn Apr 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually had started working on keeping the testroot stuff in ExhaleTestMetaclass.__new__ so that everything could stay at the same place and there would be no need for pytest setup and teardown hooks.

I finally made it work tonight after a little bit of effort and I think it's significantly cleaner. Check out https://github.com/tkhyn/exhale/blob/testing-fixture-only/testing/base.py#L135

Note: we can't merge _set_app and _rootdir because sphinx's app fixture is called in-between. It would make more sense to define _rootdir before _set_app by the way, but that's a detail

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WOW that's exactly what I tried to do but totally failed at! That commit has been added here with some other small changes / docs updates!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I actually gave up myself on my first attempt, I needed to sleep on it apparently. Everything started to become easier once I thought about adding app_params in the arguments and then overwriting srcdir in it. I think it might not even be necessary to yield testroot as the value is not used but it doesn't matter much ...

This could be made even cleaner by defining _set_app and _rootdir as partial functions out of the __new__ definition.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBH I'm very pleased with this. If you want to clean it up go for it, but I just want to build out one small CPP test case and get the class hierarchy tested (or at least a very basic version) and then put this down for a while. I need to fix the windows stuff and then table exhale for a while :S

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah nah it can stay like that, and once everything will be settle we can worry about making things super-clean 😀

@svenevs
Copy link
Owner

svenevs commented Apr 23, 2018

Note: I had to force push a commit away that I made, sorry for that :/

Repository owner deleted a comment from codecov-io Apr 24, 2018
Repository owner deleted a comment from codecov bot Apr 24, 2018
Repository owner deleted a comment from codecov bot Apr 24, 2018
Repository owner deleted a comment from codecov-io Apr 24, 2018
@svenevs
Copy link
Owner

svenevs commented Apr 26, 2018

GAH (notes to self before rage quitting on unfathomable regression):

  • CI for linux (what I developed this on...): directories are missing their file children?!!!. It should be

    ###########################################################
    ## Directories
    ###########################################################
    - [dir]: nested
      - [dir]: nested/dual_nested
        - [dir]: nested/dual_nested/one
          - [file]: one.hpp
        - [dir]: nested/dual_nested/two
          - [file]: two.hpp
      - [dir]: nested/one
        - [file]: one.hpp
      - [dir]: nested/two
        - [file]: two.hpp
    
  • I fail to see how this commit could possibly cause this

Update damn i get it. It's the whole ReadTheDocs // Doxygen dilemma all over again.

@svenevs
Copy link
Owner

svenevs commented May 3, 2018

@tkhyn WINDOWS IS FIXED!!!!!!!!!!!!!!!

This is really close to being merged now (finally.......). I just need to cleanup the flake8 errors.

@svenevs
Copy link
Owner

svenevs commented May 24, 2018

Ok I'm going to sleep on this and then do a full pass over the diff and fix up some small things like .gitignore and some small TODO that can be done at this point. Then merge and see how bloody function signatures will actually be now that there is this epic testing framework.

@tkhyn thank you very much for being willing to hash this out in such detail with me, this was a really cool experience for me. I think what we (well, mostly you...) arrived at here is quite wonderful, and will seriously help me get Exhale to where I want it to be.

I don't expect you to go through the full thing, but was there any stuff floating in your mind that needs to be addressed here still?

P.S. Sorry this went stale for a while, end-of-the-semester woes and all :/

svenevs added a commit that referenced this pull request May 25, 2018
Introduced `pytest` based framework
=================================================
- Extensive testing backend with metaclass and
  fixture magic enabling easy creation of new
  test projects and associated tests
- Not all hierarchical representations are
  complete, see hierarchies docs
- Continuous Integration!
    - Windows: AppVeyor
    - Linux / OSX / docs / lint: Travis CI
    - Coverage: codecov.io

Exhale Core
=================================================
- Exhale can now run on Windows!
    - See exhale/deploy.py doxygen execution
    - Fix pathing issues using normpath (see #30)
- Added more forceful doxygenStripFromPath logic
  as needed for Travis
- Allow spaces in names for program listings

NOTE: union nesting is less broken than it was,
      but still not valid.
Introduced `pytest` based framework
=================================================
- Extensive testing backend with metaclass and
  fixture magic enabling easy creation of new
  test projects and associated tests
- Not all hierarchical representations are
  complete, see hierarchies docs
- Continuous Integration!
    - Windows: AppVeyor
    - Linux / OSX / docs / lint: Travis CI
    - Coverage: codecov.io

Exhale Core
=================================================
- Exhale can now run on Windows!
    - See exhale/deploy.py doxygen execution
    - Fix pathing issues using normpath (see svenevs#30)
- Added more forceful doxygenStripFromPath logic
  as needed for Travis
- Allow spaces in names for program listings

NOTE: union nesting is less broken than it was,
      but still not valid.
@svenevs
Copy link
Owner

svenevs commented May 25, 2018

Ugh so I can't explicitly mark it as merged, but really want this one to show up as Green rather than Red. So I'm gonna do some force pushing here to undo my local git merge --squash. I'm also force-pushing this branch here, which loses all history. The history has already been backed up on my Exhale repo:

$ git checkout -b tkhyn-testing archive/tkhyn-testing-pr-31

In case you ever need the buildout stuff that came from the beginning or something. I needed a squash, but the GitHub message would have been ridiculous.

Anyway, this is all being done because you totally deserve the "little green square" for this!

@svenevs svenevs merged commit 17ecbd9 into svenevs:master May 25, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants