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

internal/testrunner/runners/pipeline: unmarshal test results using UseNumber #717

Merged
merged 7 commits into from
Mar 16, 2022

Conversation

efd6
Copy link
Contributor

@efd6 efd6 commented Mar 3, 2022

By default, numbers are unmarshaled as float64 resulting in low bits truncation
in longs that are above 53 bits wide, so use a decoder and UseNumber to ensure
results are not corrupted.

See behaviour before and after.

Relates elastic/integrations#2758.

@efd6 efd6 requested review from andrewkroh and mtojek March 3, 2022 23:53
@elasticmachine
Copy link
Collaborator

elasticmachine commented Mar 4, 2022

💚 Build Succeeded

the below badges are clickable and redirect to their specific view in the CI or DOCS
Pipeline View Test View Changes Artifacts preview preview

Expand to view the summary

Build stats

  • Start Time: 2022-03-16T11:31:32.344+0000

  • Duration: 20 min 36 sec

Test stats 🧪

Test Results
Failed 0
Passed 582
Skipped 1
Total 583

🤖 GitHub comments

To re-run your PR in the CI, just comment with:

  • /test : Re-trigger the build.

Copy link
Contributor

@mtojek mtojek left a comment

Choose a reason for hiding this comment

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

I understand your concerns, but I'm not sure if we need such precision here. I'd rather depend on the original JSON package implementation than introduce workarounds or enable non-standard marshaling mode.

Maybe add some test cases to this PR, so we discuss it.

@mtojek mtojek requested a review from a team March 4, 2022 09:44
@efd6
Copy link
Contributor Author

efd6 commented Mar 4, 2022

This uses the standard encoding/json package, using the method that was introduced to that package exactly for this purpose in go1.1. The change here is included to fix a real problem that can be seen in the related issue, longs are incorrectly low bit truncated. In cases where flags are stored in long, this could result in regressions due to a need to gloss over this kind of behaviour (not to mention the confusion that seeing this kind of apparent data corruption causes during development).

@jsoriano
Copy link
Member

jsoriano commented Mar 4, 2022

@efd6 I agree with the need of this change if there are cases when required precision is lost, but I am also a bit concerned of the use of a non-default mode. I see this is required in several places, and we can easily forget about this if we refactor this code, or if we add something else there that needs unmarshaling.
Would there be an option to add a test case for this? This would also help to have a sample case to reproduce issues with this.

@efd6
Copy link
Contributor Author

efd6 commented Mar 4, 2022

Yes, I'm happy to add a test case. I'm wondering how to hook that in. I'll think about how to make sure regressions are caught and add one after the weekend.

@efd6
Copy link
Contributor Author

efd6 commented Mar 7, 2022

@jsoriano Apologies for the complexity of the testing code, the level coupling here made this necessary and I figured adding test infra derived from the compiler suite would be better than a significant refactor. On the plus side, if additional tests are required, they are easy to add.

@efd6
Copy link
Contributor Author

efd6 commented Mar 7, 2022

/test

Copy link
Contributor

@mtojek mtojek left a comment

Choose a reason for hiding this comment

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

@efd6 I'm concerned about the complexity of these unit tests. WDYT about replacing them with a simple test package added here?

@efd6
Copy link
Contributor Author

efd6 commented Mar 7, 2022

I wanted to avoid the involvement of ES in order to isolate the responsibilities; allowing ES to participate in the discussion moves the complexity that exists here elsewhere and makes it more opaque. Note that the complexity is really only a gutting of (*runner).run removing the involvement of interaction with ES.

@mtojek
Copy link
Contributor

mtojek commented Mar 7, 2022

I understand that you preferred to remove the Elaticsearch from the equation, but it's natural that Elasticsearch takes place in pipeline tests. If we don't have to overengineer the test framework and achieve similar results with what we have now (for example, an extra package added to other), I would support that option.

What's your preference on this, @jsoriano?

@efd6
Copy link
Contributor Author

efd6 commented Mar 7, 2022

I'm not entirely sure that it's possible to express two of the tests here in the packages tests; from what I can see, it would be able to express the first test here run.txt which does not guard against the regressions that were the concern, but I don't see how it's possible to safely express either of the other tests which need to be outside the pipeline context to ensure that the appropriate comparison is made and to prevent a generate flag from clobbering the values silently.

The test code here could be made simpler at the cost of having to spin up the stack. If this were done, the actual (*runner).run method could be used with very little wrapping. This loses the decoupling with ES, but retains the expressibility.

@mtojek
Copy link
Contributor

mtojek commented Mar 7, 2022

but I don't see how it's possible to safely express either of the other tests which need to be outside the pipeline context to ensure that the appropriate comparison is made and to prevent a generate flag from clobbering the values silently.

Based on what you shared with us, I understood that it's easy to reproduce this case with pipeline tests and that's the kind of sample test package I suggest preparing. Could you please clarify where is the tricky part? Is it flaky or does it depend on the runtime Go environment?

@efd6
Copy link
Contributor Author

efd6 commented Mar 8, 2022

OK, if you are OK with modifying this so that you can conditionally express "should fail" (what this does) and "does not modify *-expected.json when run with -g" (what this does) then it is possible, though IMHO icky.

The second of those tests is fine with doing a diff, though that then needs to be wrapped up in an xUnit document to capture failures, but the former needs special consideration since the failure is wanted so the xUnit doc needs to be constructed to explain that a thing was wanted not to be but is (take a look at the failure stacktraces here how the testing proposed here testing could present that — not with go2xunit since it does not correctly handle test output).

@mtojek
Copy link
Contributor

mtojek commented Mar 8, 2022

The second of those tests is fine with doing a diff, though that then needs to be wrapped up in an xUnit document to capture failures, but the former needs special consideration since the failure is wanted so the xUnit doc needs to be constructed to explain that a thing was wanted not to be but is (take a look at the #721 (comment) how the testing proposed here testing could present that — not with go2xunit since it does not correctly handle test output).

Yes, but that's not how the JUnit is supposed to work and patching like that seems to be a hack for me.

Let me wear my code owner hat and propose a different approach to making this PR concise and not another test
framework:

  1. Implement positive tests using the existing framework (normal pipeline tests).
  2. Add unit tests for jsonUnmarshalUsingNumber only. We will take the risk of not covering the full path here.
  3. Document as TODOs (refactoring issue) paths we didn't cover as it requires extending the test framework. You can also add there an action item to cover negative tests.

The idea is to focus on delivering the improvement instead of exploring ad-hoc testing extensions to what we have here. Keeping it consistent help future maintainers with understanding how the codebase and not spending time on analyzing the run_generate

@efd6
Copy link
Contributor Author

efd6 commented Mar 8, 2022

Yes, but that's not how the JUnit is supposed to work and patching like that seems to be a hack for me.

I'm sorry, you've lost me here.

  1. The positive test doesn't test anything that's added here. I can guarantee this because the old code did not fail the positive test I have here.
  2. A unit test for making sure that the wrapper around json.Decodes is now necessary given that I have made it match the bytes after top level value behaviour of json.Unmarshal. I will add this.
  3. I'll add todos to the function that are left naked.

…eNumber

By default, numbers are unmarshaled as float64 resulting in low bits truncation
in longs that are above 53 bits wide, so use a decoder and UseNumber to ensure
results are not corrupted.
@mtojek mtojek self-requested a review March 8, 2022 10:46
Copy link
Contributor

@mtojek mtojek left a comment

Choose a reason for hiding this comment

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

The positive test doesn't test anything that's added here. I can guarantee this because the old code did not fail the positive test I have here.

Correct me if I'm wrong, but I understood that without this change, there might be problem with a particular pull request. I thought to copy those potentially affected/flaky/risky tests here to make sure that this PR solves the problem.

Comment on lines 226 to 227
// jsonUnmarshalUsingNumber is a drop-in replacement for json.Unmarshal that
// does not default to unmarshaling numeric values to float64.
Copy link
Contributor

Choose a reason for hiding this comment

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

Please adjust this comment to be more verbose: what kind of numbers can lose precision, when we can expect it (samples). We talked about these cases in the PR, I see a value in adding them here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added.

if err != nil {
return
}
if !reflect.DeepEqual(got, test.want) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm.. Isn't there any replacement for assert.DeepEqual in testify/assert, so we need to use reflection here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

assert.Equal calls reflect.DeepEqual in a round about way that. I try to avoid testing frameworks as much as possible unless they add value.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, in this case, it's about consistency with the rest of the codebase, so kindly please adjust to it.

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 have changed the tests so that they are testing the concern of the change here, which is that the values are properly sent through a unmarshal/marshal round-trip. This also has the advantage that the need for any kind of deep equality check becomes unnecessary. I hope this is OK. I have also added test cases that show exactly where the cutover happens in behaviour, with notes explaining how to check this, for the curious.

@efd6
Copy link
Contributor Author

efd6 commented Mar 8, 2022

Correct me if I'm wrong, but I understood that without this change, there might be problem with a elastic/integrations#2758. I thought to copy those potentially affected/flaky/risky tests here to make sure that this PR solves the problem.

Those were tested in my previous unacceptable approach. They can not be tested in an automated way without making the changes that I made or suggested. Honestly, I would not have written 200 lines of testing infrastructure if I didn't think that it was necessary to address the concerns that were expressed.

@mtojek
Copy link
Contributor

mtojek commented Mar 8, 2022

Honestly, I would not have written 200 lines of testing infrastructure if I didn't think that it was necessary to address the concerns that were expressed.

I'm afraid that in case writing tons of code without explaining context doesn't make it easier to follow or catch mistakes.

They can not be tested in an automated way without making the changes that I made or suggested.

Well, so far you didn't provide any sample test packages as we asked at the beginning of this conversation, and also here.

Those were tested in my previous unacceptable approach

The solution you provided before was partially mocking the elastic-package behavior to set specific conditions. You mentioned that it was necessary to provide infra code, but didn't show any practical, real use case where this bugfix PR solves the problem. I'd like to see in the Github description, fragments of a pipeline, package, event, etc. when the conversion can possibly go wrong and model it here.

@jlind23
Copy link
Contributor

jlind23 commented Mar 8, 2022

@efd6 sorry for jumping here quite late but is there any particular bug or behaviour that should be fixed thanks to this PR? I do not find anything in the original description.

@efd6
Copy link
Contributor Author

efd6 commented Mar 8, 2022

@jlind23 Yes, the link to elastic/integrations#2758 provides context. The relevant part is this:

Author's Checklist

  • Note the apparent mantissa bit truncation of sequence_number. This is due to encoding/json remarshalling in elastic-package as can be seen with this reproducer https://play.golang.com/p/fkTBk9V_606.

An example of the issue can be see here:

        {
            "@timestamp": "2021-03-02T09:47:13.000-05:00",
            "ecs": {
                "version": "8.0.0"
            },
            "event": {
                "category": [
                    "network"
                ],
                "code": "portal-getconfig",
                "created": "2021-03-02T09:47:18.000-05:00",
                "duration": 0,
                "kind": "event",
                "original": "Nov 30 16:09:08 1,2021/03/02 09:47:18,12345678999,GLOBALPROTECT,0,2305,2021/03/02 09:47:13,vsys1,portal-getconfig,configuration,,,domain\\maxmustermann,10.0.0.0-10.255.255.255,PC12345,10.20.30.40,0.0.0.0,0.0.0.0,0.0.0.0,8cbc136b-e262-4cf8-912c-95ea132d9fef,SERIENNR,5.1.1,Windows,\"Microsoft Windows 10 Enterprise, 64-bit\",1,,,\"Config name: GP Clients, Machine Certificate CN : (null)\",success,,0,pre-logon,0,GP Portal,6894571632887746544,0x8000000000000000,1970-01-01T01:00:00.000+01:00,,0,manual only,,",
                "outcome": "success",
                "timezone": "-05:00"
            },
<snip>
            "panw": {
                "panos": {
                    "action_flags": "0x8000000000000000",
                    "attempted_gateways": "manual only",
                    "client_ver": "5.1.1",
                    "connect_method": "pre-logon",
                    "description": "Config name: GP Clients, Machine Certificate CN : (null)",
                    "error_code": "0",
                    "priority": "0",
                    "repeat_count": 1,
                    "selection_type": "1970-01-01T01:00:00.000+01:00",
                    "sequence_number": 6894571632887747000,
                    "serial_number": "SERIENNR",
                    "source": {
                        "nat": {
                            "ip": "10.20.30.40"
                        }
                    },
                    "stage": "configuration",
                    "sub_type": "0",
                    "type": "GLOBALPROTECT",
                    "virtual_sys": "vsys1"
                }
            },

Note here that event.original has a CSV field with a value 6894571632887746544 (noted by the data source's documentation to be a unique 64-bit identifier). This is mapped to long in the package and is correctly ingested by ES as that number. However, it is written out to the expect.json file as 6894571632887747000. This is a result of encoding/json being unaware of the input type mapping and by default unmarshaling numbers as float64. The final three digit zeroing is done by the json package to avoid users ascribing validity to the low bit values since integers larger than 53 bits wide can not be represented exactly due to the width of the IEEE-754 double mantissa; this is obviously a truncation in a binary representation, but looks significant when rendered into decimal.

What this PR does is replace the calls to json.Unmarshal with use of json.Decoder with the UseNumber option with some additional work to ensure that trailing data does not exist (just using json.Decoder without this is a common bug since the decoder is intended for use with JSON streams rather than single objects). This has been standard practice for nearly a decade and I thought quite uncontroversial, though I agree that given the large number of call sites there is a potential for loss of one of the call during code evolution or addition and that protective tests would be valuable. Here comes the complexity.

The case that provoked the change here is something that cannot be tested with the current testing infrastructure; I spent quite some time attempting to fit in a test that would have made use of the code that exists, but what we want to see is that a long > 1<<53 comes through the pipeline runner unscathed, and can then be written to disk as the original long value. Because all of the code for testing here depends on dynamically typed value comparison there is no way to ensure that the looser equality check of float64 would not be used for the comparison of the long value due to some future code mutation (which was the whole point of the tests in the first place). What is needed is some ability to step outside the environment here and make sure that the data written to disk matches the expectation without reference to the mode of serialisation. That was what my testing code did, and what an alternative making use of ES with a subsequent diff of the repo would have done (though repo mutation as a test vector is a smell). As I said above, I tried to implement this without making use of the compiler suite's testscript package, but due to the degree of coupling within the (*runner).run which integrates pretty much all of the system including ES and the file system targets, this was not possible. An alternative would have been to refactor that method to reduce the coupling, but I did not feel that that would be appropriate here. Perhaps I should have sent a previous PR doing that to prepare for this.

I hope that clarifies the issue.

@jsoriano
Copy link
Member

jsoriano commented Mar 9, 2022

@efd6 thanks for providing more context. I see that some TODOs have been added for tests, and that there have been some discussions about additional test code, but I am not sure about the code being discussed, it seems that there were some force pushes. Please don't force push to ongoing reviews 🙏 it is difficult to keep track of the conversation.

I still think that we need some test here to avoid regressions on the fixed use case. I would suggest to black-box test the whole runner.Run, I see these approaches for that:

  • Add a sample package that reproduces this issue to scripts/test-check-packages.sh. It could contain a simple pipeline test only, and any regression would be detected by changes on the golden files of this sample package. I would expect a positive test to pass here only if the golden files of the package are regenerated with the misbehaving code, what could be detected on future PRs. (I think this is what Marcin proposed).
  • Add a go test for runner.Run, mocking the methods that use ES. I think that we would only need to mock installIngestPipelines that would become a noop, and simulatePipelineProcessing that would return a hard-coded result per test case. Then we could check that the written file is what we expect. We don't need to test the pipelines themselves, we only need to test that the result of runner.Run for a given response of the simulate API, and a set of expected events, is what we expect.
  • A variation of the previous strategy would be to extract the body of the main loop of runner.run to its own method that can be more easily unit tested. The we would only need to mock simulatePipelineProcessing.

Comment on lines 242 to 249
remains, err := io.ReadAll(dec.Buffered())
if err != nil {
return err
}
for _, b := range remains {
if b > ' ' || (b != ' ' && b != '\t' && b != '\r' && b != '\n') {
// Mimic encoding/json error for this case, but without rigmarole.
return fmt.Errorf("invalid character %q after top-level value", b)
Copy link
Member

Choose a reason for hiding this comment

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

Wouldn't be enough to check if there is something else there with dec.More()?

Suggested change
remains, err := io.ReadAll(dec.Buffered())
if err != nil {
return err
}
for _, b := range remains {
if b > ' ' || (b != ' ' && b != '\t' && b != '\r' && b != '\n') {
// Mimic encoding/json error for this case, but without rigmarole.
return fmt.Errorf("invalid character %q after top-level value", b)
if dec.More() {
return fmt.Errorf("unexpected characters found after unmarshaling value")
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, this won't work. Some trailing bytes are acceptable, others are not; a JSON stream can have arbitrarily large quantities of whitespace outside literals and this extends to trailing and leading data.

Copy link
Member

Choose a reason for hiding this comment

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

dec.More() ignores whitespaces, are there other acceptable bytes? The code I propose passes the test cases added in this PR (after adapting the error strings).

Copy link
Member

Choose a reason for hiding this comment

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

Or something like this could be done to keep this error message:

        _, err = dec.Token()
        if err != nil && !errors.Is(err, io.EOF) {
                r, ok := dec.Buffered().(io.ByteReader)
                if !ok {
                        return err
                }
                b, err := r.ReadByte()
                if err != nil {
                        return err
                }
                // Mimic encoding/json error for this case, but without rigmarole.
                return fmt.Errorf("invalid character %q after top-level value", b)
        }

Copy link
Contributor Author

@efd6 efd6 Mar 9, 2022

Choose a reason for hiding this comment

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

dec.More() ignores whitespaces

Nice. I didn't notice that. To retain the error parity we'll need something like the second option or what I have. I'm not convinced that the assertion to io.ByteReader is simpler of lighter though. We expect that in the happy case that remaining will be ~0, and in the unhappy case remaining will still be ~small (it must have fit in memory), but we cannot guarantee that dec.Buffered() will return an io.ByteReader, so we may miss cases.

If we are happy to lose error parity, then dec.More() would be fine.

Copy link
Member

Choose a reason for hiding this comment

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

What I would like to avoid is to do parsing here (the if b > ' ' || (b != ' ' && b != '\t' && b != '\r' && b != '\n') line), and rely on the decoder for this.

Regarding the error message, I don't have a strong opinion, but at this level I think that it is ok if we don't show the found character.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK. This will complicate the test code. I will do it next week.

Copy link
Member

Choose a reason for hiding this comment

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

No need to complicate test code, actually I think that we could simplify it, and only check that it returns an error when expected, without checking the error message.

@efd6
Copy link
Contributor Author

efd6 commented Mar 9, 2022

Please don't force push to ongoing reviews 🙏 it is difficult to keep track of the conversation.

No worries. You can see the changes that were being discussed (with the same shas) at #721 where I was using them to test an alternative to go2xunit (you can ignore the changes to the root Makefile and the added j2xunit.go file — they are not relevant here).

The second and third options that you put forward, I think, are essentially what I implemented. Please correct me if I'm wrong.

I don't think the approach of manually watching for contamination of the repo (what I understand you are suggesting in the first option) is a good idea. Without complaint from CI I can imagine this just being accidentally waved through with non-zero probability. It could be watched for by a test as I suggested above, but that seems ugly.

@jsoriano
Copy link
Member

jsoriano commented Mar 9, 2022

The second and third options that you put forward, I think, are essentially what I implemented. Please correct me if I'm wrong.

The tests I see in this PR focus on testing jsonUnmarshalUsingNumber, this is ok, but not what I refer to.

I hadn't paid much of attention to the related tests in #721, sorry. This is more aligned to what I am proposing in the second option, yes. But it looks to me that runValidation is reproducing what runner.Run does, instead of testing it. It also adds a new testing framework that I don't think we need here.

It could be watched for by a test as I suggested #717 (comment), but that seems ugly.

I don't think we would need a negative example here. If we add a new package with a pipeline that generates big integers we should be able to reproduce the situation. If some code change breaks the test for this package, it would call attention in the PR introducing the change. I think this would be enough to avoid regressions in this use case, without adding complexity to tests.

@efd6
Copy link
Contributor Author

efd6 commented Mar 9, 2022

The tests I see in this PR focus on testing jsonUnmarshalUsingNumber, this is ok, but not what I refer to.

Yeah, this is not quite true. It tests the loading and verification steps performed by (*runner).run. The logic for that is in the testscript text files in testdata.

(misclick)

But it looks to me that runValidation is reproducing what runner.Run does, instead of testing it. It also adds a new testing framework that I don't think we need here.

Yes, I was unhappy with the need to mock it substantially, but it was necessary due to coupling as I said. This could be reduced by taking closure arguments to construct the parts that are not being tested here, but that is moot given the later parts of your comment.

I don't think we would need a negative example here. If we add a new package with a pipeline that generates big integers we should be able to reproduce the situation. If some code change breaks the test for this package, it would call attention in the PR introducing the change. I think this would be enough to avoid regressions in this use case, without adding complexity to tests.

I need to think about this.

if [ "$(basename $d)" == "long_integers" ]; then
# Ensure that any change in unmarshaling behaviour is noticed; this will result in a dirty
# git state on exit if an inappropriate use of encoding/json.Unmarshal has been made.
elastic-package test -v -g --report-format xUnit --report-output file --defer-cleanup 1s --test-coverage
Copy link
Contributor

Choose a reason for hiding this comment

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

Is the -g necessary here? Shouldn't it be caught due to differences compared to the expected.json file?

Copy link
Member

Choose a reason for hiding this comment

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

+1, I think this if is not needed, elastic-package test should fail if there are regressions in unmarshalling logic. If it doesn't fail, this is a different bug to be addressed in a different PR.

Copy link
Contributor Author

@efd6 efd6 Mar 10, 2022

Choose a reason for hiding this comment

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

No, it won't be caught. This was the reason for the additional code previously. The issue is that it is not possible for a machine to measure the accuracy of some thing that is more accurate than it, and this is what that would be asking it to do; if the the code is regressed, then it becomes a less accurate machine and so become inadequate to test that the test cases (which are more accurate) are satisfied. This is all theoretically sound, but can be practically confirmed by running the tests at 9b93a10 with this change

diff --git a/internal/testrunner/runners/pipeline/test_result.go b/internal/testrunner/runners/pipeline/test_result.go
index d67853c..38916f8 100644
--- a/internal/testrunner/runners/pipeline/test_result.go
+++ b/internal/testrunner/runners/pipeline/test_result.go
@@ -228,6 +228,7 @@ func unmarshalTestResult(body []byte) (*testResult, error) {
 // prevent low bit truncation of values greater than 1<<53.
 // See https://golang.org/cl/6202068 for details.
 func jsonUnmarshalUsingNumber(data []byte, v interface{}) error {
+       return json.Unmarshal(data, v)
        dec := json.NewDecoder(bytes.NewReader(data))
        dec.UseNumber()
        err := dec.Decode(v)

They will pass.

Aleternatively, this also demonstrates the situation https://play.golang.com/p/CMsFSSjcd0f.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm.. to be honest, I'd accept that risk than introduce a sneaky workaround. It sounds to me like an edge case.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should I remove the tests? They essentially don't do anything without this.

Copy link
Contributor

@mtojek mtojek Mar 10, 2022

Choose a reason for hiding this comment

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

Let me challenge this. In this case, we're talking about the Go stdlib's nature, which unfortunately has this behavior. It means that we need to a test to detect a possible regression. I'm wondering if the unit test which calls jsonUnmarshalUsingNumber, then marshals back to []byte using the right json.Marshaller can detect this issue. WDYT?

EDIT:

https://play.golang.com/p/8xH6zmearh4 (it's similar/same? to what you have covered with your tests).

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 question that has to be asked is what does that test? Depending where it's put that could adequately test for all regressions, but as the code stands you will end up mocking essentially all of (*runner).run in order to achieve that.

There are many moving parts here, breaking some of them would be detected, but breaking others will not. The fact that breaking some of them will be is a good reason to keep the package here. I'm at the end of an extremely long day, so I don't have the cognitive capacity to sort out which are visible and which are not right now.

Copy link
Member

Choose a reason for hiding this comment

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

Ok, I see, you make a good point with the comparison of floats.

Copy link
Member

@jsoriano jsoriano left a comment

Choose a reason for hiding this comment

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

Ok, I think this is going in the good direction, but I think we don't need to modify the test script.

Comment on lines 242 to 249
remains, err := io.ReadAll(dec.Buffered())
if err != nil {
return err
}
for _, b := range remains {
if b > ' ' || (b != ' ' && b != '\t' && b != '\r' && b != '\n') {
// Mimic encoding/json error for this case, but without rigmarole.
return fmt.Errorf("invalid character %q after top-level value", b)
Copy link
Member

Choose a reason for hiding this comment

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

What I would like to avoid is to do parsing here (the if b > ' ' || (b != ' ' && b != '\t' && b != '\r' && b != '\n') line), and rely on the decoder for this.

Regarding the error message, I don't have a strong opinion, but at this level I think that it is ok if we don't show the found character.

if [ "$(basename $d)" == "long_integers" ]; then
# Ensure that any change in unmarshaling behaviour is noticed; this will result in a dirty
# git state on exit if an inappropriate use of encoding/json.Unmarshal has been made.
elastic-package test -v -g --report-format xUnit --report-output file --defer-cleanup 1s --test-coverage
Copy link
Member

Choose a reason for hiding this comment

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

+1, I think this if is not needed, elastic-package test should fail if there are regressions in unmarshalling logic. If it doesn't fail, this is a different bug to be addressed in a different PR.

Copy link
Member

@jsoriano jsoriano left a comment

Choose a reason for hiding this comment

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

@efd6 I have been discussed with Marcin offline about this PR, we are fine with going on with it so we unblock this issue. We will follow up with unit tests for the runner.

The only changes we would like to see before merging the PR are:

  • Please revert the changes in test-check-packages.sh, but leave here the sample package. As you mentioned, this won't be enough to detect regressions, but it illustrates the issue. We will try to improve detection of this kind of issues in follow ups.
  • Remove the "parsing" done when checking for invalid syntax after the message, rely on decoder for that, using More() and/or Token().

Comment on lines 242 to 249
remains, err := io.ReadAll(dec.Buffered())
if err != nil {
return err
}
for _, b := range remains {
if b > ' ' || (b != ' ' && b != '\t' && b != '\r' && b != '\n') {
// Mimic encoding/json error for this case, but without rigmarole.
return fmt.Errorf("invalid character %q after top-level value", b)
Copy link
Member

Choose a reason for hiding this comment

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

No need to complicate test code, actually I think that we could simplify it, and only check that it returns an error when expected, without checking the error message.

if [ "$(basename $d)" == "long_integers" ]; then
# Ensure that any change in unmarshaling behaviour is noticed; this will result in a dirty
# git state on exit if an inappropriate use of encoding/json.Unmarshal has been made.
elastic-package test -v -g --report-format xUnit --report-output file --defer-cleanup 1s --test-coverage
Copy link
Member

Choose a reason for hiding this comment

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

Ok, I see, you make a good point with the comparison of floats.

@efd6 efd6 requested review from mtojek and jsoriano March 15, 2022 00:19
@elasticmachine
Copy link
Collaborator

💚 Build Succeeded

the below badges are clickable and redirect to their specific view in the CI or DOCS
Pipeline View Test View Changes Artifacts preview preview

Expand to view the summary

Build stats

  • Start Time: 2022-03-16T11:31:27.609+0000

  • Duration: 3 min 5 sec

🤖 GitHub comments

To re-run your PR in the CI, just comment with:

  • /test : Re-trigger the build.

Copy link
Member

@jsoriano jsoriano left a comment

Choose a reason for hiding this comment

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

Thanks @efd6! Follow up created to improve test coverage here: #732

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.

5 participants