Add an all-in-one latex_to_pdf() function and sweet testing infrastructure - #252
Conversation
This should be helpful for people who want to embed the engine as a library. Tests and higher-level API exposure coming soon.
Along with the existing options, add a mode where output files simply aren't written to disk. Once again, tests forthcoming.
It was linked to TermcolorStatusBackend because of the note_highlighted method, but it is straightforward and reasonable to make that a trait function with a sensible default impl.
…files I *always* have trouble doing this, so here's a function that does something along the lines of what I usually want.
This gives us a nice concise example for the very top of the Tectonic API documentation. And hopefully it is actually useful for people! The testing infrastructure got a big revamp to allow us to actually execute the doctest on the new API docs. Doctest and integration-test executables are linked against the standard build of the main crate, so the supporting test infrastructure has to live in that crate and can't be gated behind a `#[cfg(test)]` attribute. I figured out a way to make this happen that I'm happy with — it makes these tests possible, but should only bloat the crate by a small amount. Importantly, this approach means that we can also make it so that the tests in `tests/executable.rs`, which launch the build `tectonic` binary, leverage the same test mode. This is a nice win — before, these tests would go to the network and pull down assets into the local user's cache. Now they use the same set of test assets as everybody else.
|
CC @Mrmaxmeier this code might be relevant to #246, if I'm understanding it correctly. The new all-in-one function might be something C users would be interested in. Also I'd be interested in any comments you have in general. |
|
Hmmm, the Circle CI test failed on the new doctests: This looks pretty clearly to not be a problem with the actual implementation. I'll try rerunning; maybe it was transient ... |
|
Woohoo, it was transient! @Mrmaxmeier @rekka Would you either of you happen to have a chance to look this over? I'm trying to be better about having my changes reviewed by someone else ... |
Codecov Report
@@ Coverage Diff @@
## master #252 +/- ##
==========================================
+ Coverage 41.12% 41.32% +0.19%
==========================================
Files 134 135 +1
Lines 60364 60952 +588
==========================================
+ Hits 24827 25188 +361
- Misses 35537 35764 +227
Continue to review full report at Codecov.
|
| /// This convenience function tries to help with the annoyances of getting | ||
| /// access to the in-memory file data after the engine has been run. | ||
| pub fn into_file_data(self) -> HashMap<OsString, Vec<u8>> { | ||
| // There must be a better way to do this ... Note that you *cannot* |
There was a problem hiding this comment.
This avoids the temporary. It can panic though:
Rc::try_unwrap(self.io.mem.files)
.expect("multiple strong refs to MemoryIo files")
.into_inner()There was a problem hiding this comment.
I think I like this more ... avoids rebuilding the hashmap. And I am pretty sure that it will never panic unless the caller has made their own copy of files, in which case they're getting what they deserve.
| /// test mode is activated in this module, the `default_bundle()` and | ||
| /// `format_cache_path()` functions return results pointing to the test asset | ||
| /// tree, rather than whatever the user has actually configured. | ||
| static mut CONFIG_TEST_MODE_ACTIVATED: bool = false; |
There was a problem hiding this comment.
An AtomicBool here would avoid the few bits of unsafe code. Not sure if it's worth it though. AtomicBools are safe to mutate by reference but slightly awkward to use.
There was a problem hiding this comment.
I'd also suggest using AtomicBool to avoid any unnecessary unsafe. Using it is not that bad.
Define using static CONFIG_TEST_MODE_ACTIVATED: AtomicBool = AtomicBool::new(false);.
Reading is as easy as CONFIG_TEST_MODE_ACTIVATED.load(Ordering::SeqCst) and storing CONFIG_TEST_MODE_ACTIVATED.store(forced, Ordering::SeqCst).
There was a problem hiding this comment.
Yes, I agree that this is better. Thanks for the suggestion!
Avoids unsafe blocks. As suggested by @Mrmaxmeier and @rekka.
|
|
||
|
|
||
| /// Where does the "primary input" stream come from? | ||
| enum PrimaryInputMode { |
There was a problem hiding this comment.
This seems to duplicate code from driver.rs. Wouldn't PrimaryInputMode::Undefined be better expressed as None for Option<PrimaryInputMode>?
There was a problem hiding this comment.
Yes, it duplicates a bit. But in both cases, the types are private to their respective modules — they are more or less implementation details of the "builder"-type objects. Because they barely come with any code, I felt it was better to duplicate a bit than to try share the type within the crate. Reasonable enough?
There was a problem hiding this comment.
That makes sense. I did not see the big picture.
| /// For more sophisticated uses, use the [`driver`] module, which provides a | ||
| /// high-level interface for driving the typesetting engines with much more | ||
| /// control over their behavior. | ||
| pub fn latex_to_pdf<T: AsRef<str>>(latex: T) -> Result<Vec<u8>> { |
There was a problem hiding this comment.
Is tectonic thread safe now? If I remember correctly, there used to be unsynchronized static globals in the C core, and the access to the bundle is not synchronized. It seems that this function lets the user run multiple instances of the engine in parallel.
There was a problem hiding this comment.
Good point. We are still not thread safe, and probably won't be for a long time: the TeX engine just has a lot of shared global state in the C code. It will take a long time to put that all into some kind of non-global struct.
At the moment I can think of two solutions:
- Bite the bullet and add locking to the engines.
- Skirt the issue and just mention in this function's documentation that it is not thread-safe in the way you describe.
I lean towards option 2. Thoughts?
There was a problem hiding this comment.
I would strongly prefer option 1. Rust selling point is "threads without data races", enforced by the type system. A quick and dirty solution is to have a Mutex that has to be acquired at the beginning of latex_to_pdf(similar to the test infrastructure currently), and add a note in the documentation that at most one instance of the engine can be running at a time and so the function might block until the other instances are finished.
There was a problem hiding this comment.
I am definitely sympathetic to that argument. I've worried a bit about possibly causing mysterious deadlocks for callers, but I'm pretty sure that can't actually happen if we implement things in a reasonable way.
If we add locking inside the crate, though, I think it should be done at a lower level: around the pieces that directly invoke the C code. Someone could create two ProcessingSession objects in separate threads and break things right now, anyway.
Yeah, I think I'm talking myself into thinking that we should bite the bullet and add this sort of locking into the crate itself:
- It's really not that complicated to do so
- Things will definitely break if you multithread
- I believe that with a single in-and-out mutex, there's no way that the locking can cause problems for callers.
There was a problem hiding this comment.
That's definitely a concern. One way to communicate this would be introducing a singleton struct, say Tectonic, that would have methods latex_to_pdf and processing_session_builder. The user would have to first create the struct using Tectonic::new(), which would return an error if Tectonic were already in use.
There was a problem hiding this comment.
I think that approach is a little less desirable because one day, hopefully, we will be thread-safe, and then the singleton Tectonic will be unnecessary and a bit inconvenient.
I'll investigate adding locking within the crate and see how it works out.
There was a problem hiding this comment.
Yeah, that was pretty easy to implement. I tend to think this is the right way to go.
Because the C/C++ engines use a ton of shared global state, they are not thread-safe. Up to this point, the `tectonic` crate didn't do anything about this: callers needed to use their own mutex to prevent engines from stomping all over each other's toes. Upon further thought and discussion, I think this approach was a bit silly. In particular, I was afraid of creating a situation where callers might find that their processes mysteriously deadlocked, but I don't think that's actually possible. We just have one mutex, and it is acquired and released in a totally straightforward manner. I have trouble seeing how any trouble might arise. ... Famous last words?
|
OK, this PR now adds an internal mutex in the Tectonic crate as well as the other changes building up to the all-in-one function. The Circle CI build failed again due to the same error as before, which was transient the first time. I've relaunched it. If this failure happen frequently we'll need to figure out some way to mitigate it. Any final comments? |
|
OK, will need to investigate the QEMU/big-endian doctests, but let's go ahead and merge this. |
match single line
The front page of our API docs will now look way better! And the "executable" tests now have better behavior, not touching the local user's cache or the network.