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

add doctest = :fix option to makedocs #656

Merged
merged 1 commit into from
Mar 13, 2018
Merged

add doctest = :fix option to makedocs #656

merged 1 commit into from
Mar 13, 2018

Conversation

fredrikekre
Copy link
Member

@fredrikekre fredrikekre commented Mar 5, 2018

This makes it easier to fix multiple outdated doctests in one command, which is useful to do e.g. when preparing a new release.

Not really sure how to test this.

fix #448

@fredrikekre
Copy link
Member Author

Will do something for #448 (comment) too.

@fredrikekre
Copy link
Member Author

fredrikekre commented Mar 5, 2018

Added a commit that looks for the complete code-snippet before doing the replacement instead of just searching for the result. This should eliminate the risk of double replacement as described in #448 (comment) in 99.9% of cases.

@fredrikekre
Copy link
Member Author

Actually, this will still turn

```jldoctest
julia> 1 + 1
3

julia> 1 + 2
3
```

into

```jldoctest
julia> 1 + 1
2

julia> 1 + 2
2
```

@fredrikekre
Copy link
Member Author

Okay, this version should work in all (?) cases:
First search in the file for the whole code snippet (rather than just the output string), then search within the code block for the input string, and last replace the old output with the new one.

Will add some tests, but feel free to review the implementation.

@fredrikekre
Copy link
Member Author

Turns out the method I described in the last comment still failed in for the following case:

```jldoctest
julia> 1 + 2
2

julia> 1 + 2
2
```

because when we try to fix the second fail in this block we can not find that block in the file, since we replaced it with

```jldoctest
julia> 1 + 2
3

julia> 1 + 2
2
```

I solved this by storing the full Markdown.Code object in Result and then when replacing the content in the file we also replace the content in the Markdown.Code object such that the internal representation mathces that in the file.

I have now also added tests with the corner cases I have found when working on this.

@@ -343,8 +353,8 @@ function report(result::Result, str, doc::Documents.Document)
println(ioc)
printstyled(ioc, "> File: ", result.file, "\n", color=:cyan)
printstyled(ioc, "\n> Code block:\n", color=:cyan)
println(ioc, "\n```jldoctest")
println(ioc, result.code)
println(ioc, "\n```$(result.block.language)")
Copy link
Member Author

Choose a reason for hiding this comment

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

Storing the full Markdown.Code object also allows to print the actual language field here, including name, filter and setup. Previously it was a bit confusing that those things were left out from the error printing.

Copy link
Member

@mortenpi mortenpi left a comment

Choose a reason for hiding this comment

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

This is neat, thanks for this!

There's an edge case it can't handle at the moment (it fixes the wrong doctest):

```@meta
DocTestSetup = quote
    foo() = 42
end
```

```jldoctest
julia> foo()
42
```

```@meta
DocTestSetup = quote
    foo() = 43
end
```

```jldoctest
julia> foo()
42
```


To fix outdated doctests, the `doctest` flag to [`makedocs`](@ref) can be set to
`doctest = :fixup`. This will run the doctests, and overwrite the old results.

Copy link
Member

Choose a reason for hiding this comment

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

Might be good to recommend here that the user should git commit their code changes, just in case, before running the doc build with this option on. That way, should the find-and-replace algorithm go a bit wonky and completely mess up the source file, the user can easily just git reset --hard to their state.

To fix outdated doctests, the `doctest` flag to [`makedocs`](@ref) can be set to
`doctest = :fixup`. This will run the doctests, and overwrite the old results with
the new output.

Copy link
Member

Choose a reason for hiding this comment

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

Might be good to recommend here that the user should git commit their code changes, just in case, before running the doc build with this option on. That way, should the find-and-replace algorithm go a bit wonky and completely mess up the source file, the user can easily just git reset --hard to their state.

test/doctests.jl Outdated
str = read(f, String)
@test str == good_str
end
end
Copy link
Member

@mortenpi mortenpi Mar 6, 2018

Choose a reason for hiding this comment

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

This should fix doctests in .jl files too, right? Could perhaps also test that, to make sure that the file paths get resolved correctly etc.

@fredrikekre
Copy link
Member Author

fredrikekre commented Mar 6, 2018

Yea thats a corner case I also thought of. But it should be pretty rare(?) that the same input yields different output. Will add the recommendation to git commit and also recommend that the result should be inspected manually.

@mortenpi mortenpi added this to the 0.15.0 milestone Mar 6, 2018
@mortenpi
Copy link
Member

mortenpi commented Mar 6, 2018

But it should be pretty rare(?) that the same input yields different output.

Agreed. As you said, a note in the docs saying that there are potential edge cases etc. should be enough.

@fredrikekre fredrikekre force-pushed the fe/doctest-fix branch 2 times, most recently from 43d5e19 to d76fcd9 Compare March 6, 2018 08:45
@KristofferC
Copy link
Member

Great stuff to have!

@fredrikekre
Copy link
Member Author

Hmm, this will not actually work for script-doctests....

@fredrikekre
Copy link
Member Author

Ok, we are good I think.

src/DocChecks.jl Outdated
# TODO: decide whether to keep `# output` syntax for this. It's a bit ugly.
# Maybe use double blank lines, i.e.
#
#
# to mark `input`/`output` separation.
input, output = split(code, "\n# output\n", limit = 2)
result = Result(code, "", output, meta[:CurrentFile])
input, output = split(block.code, "\n# output\n\n", limit = 2)
Copy link
Member Author

@fredrikekre fredrikekre Mar 6, 2018

Choose a reason for hiding this comment

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

Any dangers with this? The problem was that output here became e.g. "\n2" while the result string became "3", thus we replaced

```jldoctest
1 + 2

# output

2
```

with

```jldoctest
1 + 2

# output
3
```

instead of

```jldoctest
1 + 2

# output

3
```

Copy link
Member Author

Choose a reason for hiding this comment

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

Perhaps it would be safer to do

input, output = split(block.code, "# output\n", limit = 2)
input = rstrip(input, '\n')
output = lstrip(output, '\n')

?

Copy link
Member Author

Choose a reason for hiding this comment

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

From the manual:

Note that the amount of whitespace appearing above and below the # output line is not significant and can be increased or decreased if desired.

So we should strip '\n' instead

Copy link
Member

Choose a reason for hiding this comment

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

We should also support Windows line endings here I think, which would not get stripped correctly at the moment, right? This might work though?

input = rstrip(input, ['\n', '\r'])
output = lstrip(output, ['\n', '\r'])

@fredrikekre
Copy link
Member Author

Perhaps we should change :fixup -> :fix. I like doctest = (true|false|:fix) better I think. Thoughts?

@mortenpi
Copy link
Member

mortenpi commented Mar 8, 2018

Yup, :fix looks better.

@fredrikekre fredrikekre changed the title add doctest = :fixup option to makedocs add doctest = :fix option to makedocs Mar 8, 2018
@fredrikekre
Copy link
Member Author

doctest = (:yes|:no|:fix) might be even better, but w/e. Changed :fixup -> fix.

src/DocChecks.jl Outdated
truncate(f, 0)
# first look for the entire code block
codeidx = findnext(code, content, 1)
@assert codeidx != 0:-1 # this should never happen
Copy link
Member

Choose a reason for hiding this comment

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

If you have CRLF line endings then this throws. It also leaves the source file empty on error right now.. so even if we fix this particular case, I think it would probably be a good idea to try ... catch all of this and restore the original file when are any errors (and then rethrow).

Copy link
Member Author

Choose a reason for hiding this comment

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

Good catch. We could potentially try to detect the line ending style, unless there could sometimes be a mix of the two?
Probably good to wrap in try catch anyway

@fredrikekre
Copy link
Member Author

Pushed a new version that should fail more gracefully if we don't find the code block. Also leaves the source file untouched in case of failure.

@fredrikekre fredrikekre force-pushed the fe/doctest-fix branch 3 times, most recently from 8a14599 to b64a4d7 Compare March 11, 2018 16:06
return
end
# write everything up until the code block
write(io, content[1:prevind(content, first(codeidx))])
Copy link
Member

Choose a reason for hiding this comment

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

I was able to trigger a

ERROR: LoadError: BoundsError: attempt to access "# Test\r\n\r\n```jldoctest\r\njulia> 2 + 2\r\n0000\r\n```\r\n"
  at index [0]
Stacktrace:
 [1] prevind(::String, ::Int64, ::Int64) at ./strings/basic.jl:426
 [2] prevind at ./strings/basic.jl:421 [inlined]
 [3] fix_doctest(::Documenter.DocChecks.Result, ::SubString{String}, ::Documenter.Documents.Document) at /home/morten/.julia/v0.7/Documenter/src/DocChecks.jl:395
 [4] checkresult(::Module, ::Documenter.DocChecks.Result, ::Dict{Symbol,Any}, ::Documenter.Documents.Document) at /home/morten/.julia/v0.7/Documenter/src/DocChecks.jl:305

with CRLFs (this file) right now. So this manages to avoid your checks. Sorry for keeping on breaking this 😆

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm.. How? I simply get:

 !! Could not find code block in source file

Copy link
Member Author

Choose a reason for hiding this comment

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

Could it be that you don't have JuliaLang/Compat.jl@5ff9892 ?

Copy link
Member

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, my bad. I happened to be running this with my 0.7 binary and I think it is a bit too old (9 days).

codeidx = Compat.findnext(code, content, 1)
if codeidx === nothing
Utilities.warn("Could not find code block in source file")
return
Copy link
Member

Choose a reason for hiding this comment

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

This check (and the one below) should still never happen, right? Perhaps we should keep throwing/erroring/asserting here so that in case they are triggered, they don't go unnoticed.

Copy link
Member Author

Choose a reason for hiding this comment

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

My thought here was that since we have replaces \r\n with \n in the code block

# Normalise line endings.
block.code = replace(block.code, "\r\n" => "\n")
we would obtain nothing here since we have not performed the replacement in the file where we search.

test/doctests.jl Outdated
\"\"\"
foo() = 1
end # module
"""
Copy link
Member

Choose a reason for hiding this comment

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

Having a test with CRLFs wouldn't hurt. We could also keep these in separate files and just cp them over to tempdir to avoid problems with differing line endings in a single file (although a simple replace(..., '\n' => "\r\n") works as well I guess).

@fredrikekre
Copy link
Member Author

So currently this will just do nothing for CRLF files. It is trivial to support this, by simply normalizing the lineendings when reading the source file before we search for the code block. The problem though is that when we print it back to the file we have updated all line endings. We could do this, or somehow try to detect which linendings are used and replace with that before printing back to file. That will however be hard if a file has a combination of lineendings.

@mortenpi
Copy link
Member

I don't think it should be changing line endings of unrelated lines. But we would only need to figure out the line ending for each doctest separately, right? Could we, say, count the LFs and CRLFs to determine which one it is and then normalize to that? I think we could live with changing a mixed doctest to either pure LF or CRLF.

In any case though, I'd be happy to merge this with just LF fixing as well, and it can be improved in the future if there's demand. It will already be massively useful for Base which is LF. Should just probably have a note in the manual about CRLFs.

@fredrikekre
Copy link
Member Author

It is a bit tricky since the byte index in the string will change after we have replaced the line endings. I added a note that this currently only works for LF line endings for now.

Also added some better tests, including multiline output which was missing before.

@fredrikekre
Copy link
Member Author

I have tried this with Julia base and some other projects and it works well for real projects too, not just the trivial test cases in the tests, so if you are happy with the current state, feel free to merge and we can work out possible problems as we find them.

@fredrikekre
Copy link
Member Author

I should just mention that it does not quite work together with doctest filters, but perhaps easier to review the changes to make that work in a separate PR.

@mortenpi
Copy link
Member

Yup, let's iterate on this in future PRs, if the need arises. Thanks a lot for this, this is a really neat feature!

@fredrikekre
Copy link
Member Author

This is a pretty useful feature when updating packages, doing this Ferrite-FEM/Tensors.jl@213ac2c manually would take some time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Doctest "apply fix" flag
3 participants