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] Automatic Linux test manifest generation #156

Closed
wants to merge 23 commits into from
Closed

[WIP] Automatic Linux test manifest generation #156

wants to merge 23 commits into from

Conversation

czechboy0
Copy link
Member

https://bugs.swift.org/browse/SR-710

This is first stab at automatically generating Linux test manifests. Generation seems to be working correctly, but I haven't written any tests yet, so it's not ready to merge. I wanted to present this early to get feedback.

  • Please note that at the moment I'm using naive string-based parsing of the test classes to find test cases. Even though it's simple, I haven't yet found a case which it can't handle. Still, AST-based discovery is the long term goal here (and I hear @aciidb0mb3r is already working on it).
  • Note that the generation only happens on Linux, so if you want to test it out on a Mac just comment out the couple of ifdefs in describe() that have anything to do with test manifests.
  • By comparing running tests before and after the changes in this PR, I noticed that on Linux SwiftPM already wasn't running 2 tests that did run on OS X (124 Linux vs 126 OS X). This is just another reason to move to automatic generation, because missing tests might lead to a false sense of security. Now all 126 tests are ran on both platforms.

DONE

  • in build phase automatically generates XCTestMain.swift file (replacement for LinuxMain.swift) for each test product and a XCTestManifest.swift file for each module. The files are now generated in folder .build/xctestmanifests, meaning we don't add any user-visible files.
  • all test classes in XCTestMain are now scoped by their module, so there's no issue when two modules have the same test class name.
  • swiftpm's own tests are getting generated correctly, so this PR removes the manual Linux XCTestCaseProvider extensions and LinuxMain.
  • XCTestManifest.swift for each module are now correctly added to the module's sources. I had to add a function on Module to allow for adding sources after creation with an array of absolute paths.
  • originally this PR generated the test manifest files next to user's test cases, but that was later changed to generate these files in .build, to avoid users having to manage those files with SCM etc.
  • no migration planned, as @mxcl pointed out, we're allowed to make a breaking change like this this early in the development process
  • where should this code go? (A new module, which I call testGen for now. Name suggestions welcome)

TODO with Questions

  • look into using an actual AST parser (e.g. swiftc -dump-ast), @aciidb0mb3r working on that
  • unit tests

@modocache
Copy link
Contributor

Wow, this is amazing! Thanks for this, and thanks for linking to it on SR-710. 👍

//imports
try fputs("import XCTest\n", file)
try metadata.dependencies.forEach {
try fputs("@testable import \($0)\n", file)
Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry if I'm missing something, but do we need to import dependencies at all here? As far as I understand this...

  • ...we're writing a test manifest that will be part of the test package that declares all the test cases and methods, correct? So we can reference the test cases directly, without an import.
  • ...the test cases and their test methods won't reference their dependencies. So we don't need to import them.

Am I missing something?

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, I think you're right. I just got too influenced by LinuxMain.swift's structure. I removed it, thanks!

@modocache
Copy link
Contributor

Again, this is really great. I'm very impressed with how much boilerplate this eliminates, even in SwiftPM's codebase alone! 😍

One thing I'd like to request is a way of "opting-out" of this feature. Some third-party testing frameworks have their own custom systems for building a XCTMain() invocation--here's how Quick does it. This sort of automatic generation of manifest files would fail for test packages written using Quick.

I understand that SwiftPM is only concerned with XCTest integration for now, but I think allowing for an opt-out now would make it easier to support more frameworks in the future. Thoughts? 🙌

@czechboy0
Copy link
Member Author

@modocache I thought about this, because I support the proposition of making SwiftPM open to alternative testing frameworks in the future.

The way the generator works now is that

  • if no XCTestCase subclass is found in a test module folder, no LinuxTestManifest.swift is created and
  • if no module provided any valid XCTestCase subclass, no LinuxMain.swift file is generated.

So as far as I understand, if you don't have any XCTest subclasses in your test, nothing will get generated. Is that enough to not break Quick-based test modules? Otherwise we can talk about ways of opting out.

@czechboy0
Copy link
Member Author

Now the newly generated files are correctly added to each module's sources, so please give it a try on your projects. Verify that the same number of tests are run on OS X and Linux.

This works for swiftpm itself. No need for manual XCTestCaseProvider extensions anymore.

@modocache What do you think about my question re where we should keep these files? I'm leaning towards generating them in .build, but I want to make sure I run it by someone else before I go and make the changes.

/// Splits string with any character in passed-in string of delimiters
public func splitWithCharactersInString(delimiters: String) -> [String] {
let stops = Set(delimiters.characters)
return self.characters.split { stops.contains($0) }.map(String.init)
Copy link

Choose a reason for hiding this comment

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

No need to wrap the stop.contains in a closure, could pass the contains func directly.

- characters.split { stops.contains($0) }.map(String.init)
+ characters.split(stops.contains).map(String.init)

Copy link
Member Author

Choose a reason for hiding this comment

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

Unfortunately that's trying to match the wrong split and doesn't compile. I'd have to do

characters.split(isSeparator: stops.contains).map(String.init)

which reads worse to me.

Copy link

Choose a reason for hiding this comment

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

Ah, too bad. 👍

Copy link
Member Author

Choose a reason for hiding this comment

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

But thanks for pointing it out, I always forget you can just pass the function in. 👍 I only tried this after you commented here, couldn't get it to work though.

@modocache
Copy link
Contributor

So as far as I understand, if you don't have any XCTest subclasses in your test, nothing will get generated.

Ingenious!! Sounds great. And I admit I've yet to think about projects that include a mix of XCTestCase and Quick tests--I'm not even sure our mechanism works for those right now, so in we definitely shouldn't worry about it here.

Thanks!! Very excited for this pull request.

@czechboy0
Copy link
Member Author

@modocache Cheers, thanks for your comments and advice here 👍

@modocache
Copy link
Contributor

I defer to @mxcl on where to keep the files. My vote is tentatively for .build. I'd be annoyed if generated code was included in my source tree and subject to whatever version control software I used. (I guess everyone writing Swift could start adding LinuxTestManifest.swift to their .gitignore but this seems bad to me.)

@aciidgh
Copy link
Contributor

aciidgh commented Feb 28, 2016

@czechboy0 @modocache Awesome work!

I wanted to mention that I've been working on AST generation and parsing for the same bug.

So far I've been able to generate AST of tests modules by adding a "tests-ast" target in debug.yaml to generate ASTs via swift-build-tool and then parse it when swift-test is run. Its working but needs more work in parsing I guess and then file generation.

Anyway @czechboy0 do you think we can combine to efforts here?

@czechboy0
Copy link
Member Author

@modocache Agreed, especially since the manifests are clearly anti-DRY, the truth is in fact in the tests themselves, so I view these as just intermediate build products.
@aciidb0mb3r Absolutely! I even designed this PR around the idea that AST parsing will come later without the need to change the generation. This is where I imagined you could plug it in (feel free to branch off of this and then PR your AST parser back into this branch).

@aciidgh
Copy link
Contributor

aciidgh commented Feb 28, 2016

@czechboy0 great! Awesome.

Also, I feel the generator and parser belongs in Multitool rather than Build, thoughts?

@czechboy0
Copy link
Member Author

Very possible, I built it from the integration point out, so that's why it's in Build now, but I'm happy to move it wherever you and @mxcl think is best.

@czechboy0
Copy link
Member Author

I made the change so that now instead of generating test manifests and LinuxMain in the Tests directory among the user's test files, we now generate them in .build/xctestmanifests. This allows for this process to be completely transparent for users on Linux.

@czechboy0
Copy link
Member Author

@modocache If this gets merged, how are we going to handle the documentation in Swift XCTest? Technically, when using SwiftPM, users will be discouraged from adding allTests methods. E.g. here https://github.com/apple/swift-corelibs-xctest/blob/master/README.md#additional-considerations-for-swift-on-linux It might be premature, but I was wondering what you thought.

@mxcl
Copy link
Contributor

mxcl commented Feb 29, 2016

Nice work.

We definitely need to use the AST. For example: this was #if os(Linux) will be parsed and incorporated into the generated output.

I have been discussing this with some others at Apple and we feel that the best way to approach this is to add a new AST output mode to swiftc by extracting out some of the code from SourceKit and putting it into libIDE.

It should be an undocumented option, that SwiftPM uses exclusively on Linux.

Thus the AST output can be the minimum we require.

The reason we cannot depend on the output from -dump-ast is this is not a stable form, we need to make our own stable form.

how should we handle projects that already have these files created manually? Currently if the project already has these files, compilation will later fail on duplicate symbols (the compiler will find two identical extensions of XCTestCaseProvider). We need to migrate users over to not managing these manually, so I suggest we detect their manual manifests during parsing and throw a very specific error with description of what they need to do.

IMO, doing nothing is fine. We're an early product and have the liberty to make breaking changes.

where should this code go? At the moment the new files live in the Build module, but @aciidb0mb3r mentioned there might be a better place to put them. I'm all ears.

A new module is good IMO. I have no name in my head yet.

@mxcl
Copy link
Contributor

mxcl commented Feb 29, 2016

Also, we'll need to watch this PR #158

@czechboy0
Copy link
Member Author

Also, we'll need to watch this PR #158

I looked at that and the generator will require only a small change in the files it produces. It already has all the information it needs (test class names, test function names and test module name) in the generator, so those PRs can be merged and then I'll make the change here.

A new module is good IMO. I have no name in my head yet.

How does testGen sound?

to add a new AST output mode to swiftc

Do you have an idea of who will work on that? I understand that will be a prerequisite of @aciidb0mb3r 's AST parser working properly, thus of this PR (unless you'd willing to merge the current version with the string-based parser, which, as you mentioned, doesn't respect #ifdef's).

@briancroom
Copy link
Contributor

@czechboy0 this is really cool work! I'm very glad you've opened this PR with such a convincing demonstration of how viable this approach is right now. Here are a few thoughts I have:

  • Regarding opt-out, what if we took the presence of an already-existing LinuxMain.swift as a sign to skip all auto-generation for the product?
  • I realize that you are following the patterns used in use elsewhere in the codebase, but I'm wondering if we can move away from explicitly marking these generated files as Linux, in light of how much progress is being made on additional platform ports already. I'm not so certain how to name them instead, but I'd suggest XCTestMain.swift and XCTestManifest.swift as a starting point. The conditional compilation in these files could then check for it being a non-Apple platform.
  • Here's an off-the-wall idea: perhaps portions of this functionality would be better living outside of SwiftPM itself, and rather as a standalone tool under the swift-corelibs-xctest umbrella. Similarly to how Xcode XCTest ships with the xctest test runner binary, there could be a xctest-gen (or similar) tool which could take a directory or list of source files and generate a test manifest and/or main.swift file in a specified output location. It seems like it should be possible to construct it such that the generator tool would need little or no knowledge of SwiftPM implementation details, and it would help keep SwiftPM slimmer and more modular. Thoughts? (also @mike-ferris-apple).

@czechboy0
Copy link
Member Author

@briancroom Thanks!

Regarding opt-out, what if we took the presence of an already-existing LinuxMain.swift as a sign to skip all auto-generation for the product?

That sounds very reasonable, however I'm wondering what the general direction of these files is. The advantage of what you're suggesting is that existing packages won't have to change, but I also consider that a long term burden, because we'll be creating divergence already. I think pushing everyone to the same approach, whatever that is, is preferable in the long term. But if not breaking existing packages becomes a requirement here, your suggestion is the best one I've seen so far.

if we can move away from explicitly marking these generated files as Linux

Absolutely, I thought about this too before, I haven't come up with a good name yet. XCTestMain.swift and XCTestManifest.swift actually sound very good to me! Would that make sense @mxcl?

perhaps portions of this functionality would be better living outside of SwiftPM itself

Although I'd prefer that too, currently the generator is using and modifying the Module and Product structure and thus directly affecting what goes into compilation (we need to add the files to be compiled and linked with their corresponding test modules and test products). There might be middle ground - generate a separate binary, which can be triggered independently, but uses dependencies from the SwiftPM project and lives in its repo (and is sometimes triggered by it). Thoughts, @mxcl?

@aciidgh
Copy link
Contributor

aciidgh commented Mar 1, 2016

@czechboy0 Hey can you squash your commits ? I'm going to start working on integrating this with ast parser

aciidgh and others added 18 commits March 1, 2016 09:31
…ain way of adding them to the product is too late, they need to be in each of their module already before that
…eir module. I don't see any harm in always scoping, in fact it is clearer which class comes from which module. And since this is generated people won't be looking at it much anyway.

removed manual linux XCTestCaseProvider extensions

[Parsing] handle test classes with modifiers

[Parsing] now checking for trailing () to verify test function (well, -ish). Also ignoring LinuxTestManifest.swift when going through test files, but that's just for good practice
[Test manifest] removed an old manual Linux test manifest

[Generator] We don't actually need to import the dependencies in the LinuxTestManifest files.

[Design] Moved Array extension back into the only file that uses it.

[Generator] Don't generate LinuxMain.swift if no test classes are to be written.

[Parser] Handle e.g. "func testMe(){" where there's no whitespace before {

[Describe] Now properly adding manifests to their module source files, thus everything should be working on first build/test. Turns out not that much hackery was necessary.

[Fix] Immutability warning fixed

[Typo] fix

[Experiment] Generating files in .build/.xctestmanifests instead of next user's files

Make xctestmanifests not hidden in .build

No need to check for already added manifest file to a module because these files are not mixed with user files anymore

Using the new protocol Buildable's isTest
Change if to guard

Add \n at EOF

Update SourceLayouts.md
# Conflicts:
#	Tests/LinuxMain.swift
#	Tests/PackageDescription/PackageTests.swift
@czechboy0
Copy link
Member Author

@aciidb0mb3r My squashing attempts went all over the place, so I reopened the PR on a new branch. Please follow #159 from now on. Apologies everyone.

aciidgh pushed a commit to aciidgh/swift-package-manager that referenced this pull request Jan 11, 2019
…d-result

Revert "Change DB API to find constant key IDs."
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.

7 participants