Directly test a stream of opcodes#298
Conversation
|
I don’t think I can re-trigger the workflows (or I can’t see the button!) but I think you can @IsaacWoods - it should run OK now |
There was a problem hiding this comment.
Nice, thanks for working on this! There are probably a few more places it would be convenient to test against encoded streams.
A few comments re organisation.
Edit: I can re-trigger the workflow, but it looks like it just re-pulls the PR branch. I think to test against the new main including #293, you would need to rebase and force-push.
| } | ||
|
|
||
| fn pkglength(&mut self) -> Result<usize, AmlError> { | ||
| let lead_byte = self.next()?; |
There was a problem hiding this comment.
This is another tricky case re improving testability vs increasing cognitive overhead to the interpreter.
I don't think the payoff of the separate module + the closure adds enough value vs being able to see this instinctually as a loop munching a variable-size length specifier. Breaking pkglength decoding would so fundamentally break the AML tests that I don't think unit tests add much here.
| /// the pkglength field that gets output. This function adds that extra length. | ||
| /// | ||
| /// Returns an error if length >= 2^28. Otherwise, returns the encoded pkglength in a vec, LSB-first. | ||
| pub fn encode(data_length: u32) -> Result<Vec<u8>, PkglengthTooLongError> { |
There was a problem hiding this comment.
I think this probably belongs in test_infra rather than in the interpreter, at which point it can just panic instead of having an error type.
| /// opcodes to execute. | ||
| #[allow(dead_code)] | ||
| pub fn run_opcodes_test(opcodes: &[u8], handler: impl Handler) { | ||
| // This function is very similar in structure to `run_aml_test`, but whilst there are only two |
There was a problem hiding this comment.
A nit: commenting on code structure here distracts the reader from the logic itself
| // DefMethod := MethodOp PkgLength NameString MethodFlags TermList | ||
| let mut method_bytes: Vec<u8> = vec![0x14]; | ||
| // PkgLength - add 5 to cover the length of the method name and flags. | ||
| method_bytes.extend(encode(opcodes.len() as u32 + 5).unwrap().iter()); |
There was a problem hiding this comment.
As above, I think we should probably have the logic to encode pkglengths in the testing logic rather than the interpreter. Having this as encode in the namespace is also a bit confusing (you would not know this was for a pkglength if not for the comment).
Idle thoughts: I don't know if we'd want to go this far, but one option would be to have this be a wrapper over Vec<u8> that is a sort-of AML builder pattern. Something like:
builder.push(Opcode::Method);
builder.push_pkglength(x);
builder.push_namestring(b"MAIN");
...There was a problem hiding this comment.
The first part, I can partially fix encode by making it pkglength::encode. See my later comment re organisation.
For the second part: yes, like your thinking for the future. Naturally I'd probably extend it by adding pkglength and Namestring types with a ToOpcodes (or similar) trait, and then having push be generic across impl ToOpcodes. Or something like that.
Use this to add a regression test for rust-osdev#289
9928f16 to
95c17bd
Compare
Ah cool - live and learn. I can see the workflow succeeds now |
|
I've made a minor update and rebased, but I want to push back on organisation and hear your thoughts. To be up front, these are my two main points of view - I think you disagree, but please bear with me:
Starting with testing, and in particular your comment
I suppose the ideal is that if you fiddled with it as part of a larger project, you'd know straight away that it was wrong, rather than finishing working on everything and only finding out at the end you'd made a bad assumption and had to fix a load of stuff. But also, it's not a given that "obvious" breaks will get caught quickly - even if in this case it probably would. Allow me to tell you a war-story (sorry, boring neckbeard alert!) Once upon a time, whilst I still coded for a living, I did some "low-risk" refactoring around some password-verification code as part of a project relating to allowing one person to use multiple VoIP phones at once. Long story short, I inverted the result of that check (i.e. wrong password = OK, correct password = Access Denied). This commit survived:
and only got caught by a much more in-depth overnight multi-hour test suite. Of which 75%+ of tests now failed. I got an angry call from the QA manager demanding to know why I was such an unbelievable imbecile. So now I try to make sure even "foundational" code gets tested, so my screw-ups are caught ASAP. But deeper than this, I'd say it's important to separate responsibilities as much as possible - On top of that, when you're working on A tangential point on testing and separating responsibilities: some guidance given to me by a mentor was "if you think about how you'll test your code, that will help you design your code". OK, so that's my argument for why I'd like the decoding to be separate from Pragmatically,
Does that make the whole One final specific - why put "encode" and "decode" together? Encoding isn't really a test-specific thing, but it is an AML-specific thing. So IMO it lives somewhere within the Plus keeping encode and decode together has made them way easier to test! So anyway, lots of writing there. Does that change your thinking at all? It is your crate to be the boss of, of course! |
|
Thanks for taking the time to write your thoughts out - it's always useful to get other perspectives, so I appreciate that.
On reflection, this is very fair. I've been surprised (and often slightly embarrassed) at the number of bugs that you've found from your more thorough testing. I do think in this case, we would notice breakage very easily, but there is of course value in testing this regardless. I still stand by a dislike for testing that necessitates changes to code structure, particularly abstraction that is unnecessary in the base design. In this case, that it needing to abstract getting the next byte of the stream via a passed function, as outside of testing that will always fundamentally be the next byte from the current stream. However, I think we can avoid this and allow unit tests as I'll talk about below, in a way that hopefully we can agree provides value.
This is also fair. At the start of the interpreter rewrite, I was very wary of committing to much of a structure bar "the interpreter" as the thing that hamstrung my previous attempts had been AML's relentless habit of needing to access interpreter state / jump back into interpreting arbitrary AML when you least expect it. I have still managed to fall into this trap (currently, parsing of field lists cannot deal with arbitrary AML that can appear in niche However, that has also created a familiarity for me that is of course not at all universal - I know my way around the interpreter as is, but I can appreciate that The This is more off-topic for this PR, but I may as well set out my thinking so far re where I see breaking up
In terms of testing, having some sort of
This is the bit I'm not coming round to, I don't think. I think the scope of the |
This PR relies on #293 being merged, otherwise the new test will fail Expect a "pipeline failed" email for this PR.
Adds the ability to specify a stream of opcodes to test. These are wrapped in a
DefinitionBlockandMAINfunction. The new test method callsMAINin the same way as existing tests (0 or undefined being "pass" results).This allows us to regression-test #289.
Since we now need to encode
pkglength, I've moved all pkglength code (including decode) into its own module and added some tests for it.