Skip to content

Add helper function for getting the source edits of two strings #1662

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

Conversation

StevenWong12
Copy link
Contributor

This change should make it easier to test two different source by getting SourceEdit with the helper function

@StevenWong12 StevenWong12 requested a review from ahoppen as a code owner May 15, 2023 03:19
Copy link
Member

@ahoppen ahoppen left a comment

Choose a reason for hiding this comment

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

Thanks, this seems like a good helper function to write incremental parsing tests. I’ve got a few comments inline.

SourceEdit(offset: 7, length: 0, replacementLength: 1),
]

XCTAssert(diffs == expectedDiffs)
Copy link
Member

Choose a reason for hiding this comment

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

One thing you could also test here is that applyEdits(diffs, concurrent: false, s1) is something close to s2 but with a few questions marks of where characters are inserted.

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 think applyEdits is written in SwiftSyntaxTest and we're in SwiftParserTest here so I could not directly call it. If that's necessary I could move it to elsewhere.

Copy link
Member

Choose a reason for hiding this comment

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

Actually, now that I think about it, it would make sense to move getConcurrentEdits to _SwiftSyntaxTestSupport and move all of your new tests in Tests/SwiftSyntaxTestSupportTests because they are really testing that this helper function actually works. You can then also move applyEdits to _SwiftSyntaxTestSupport and use it from your new tests.

@StevenWong12 StevenWong12 force-pushed the add_helper_function_for_getting_sourceedit branch 2 times, most recently from 0b8889d to ebf0b32 Compare May 16, 2023 10:22
@StevenWong12 StevenWong12 requested a review from ahoppen May 16, 2023 10:23
@@ -14,33 +14,108 @@ import XCTest
import SwiftSyntax
import SwiftParser

/// Returns the `ConcurrentEdits`s to transition from `base` to `new`.
func getConcurrentEdits(from base: String, to new: String) -> ConcurrentEdits {
Copy link
Member

Choose a reason for hiding this comment

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

I’d make this function fileprivate because usually I don’t like it if test cases suddenly start relying on helper functions from other files. If we need this in multiple test files (which we don’t right now), I usually prefer to move it to a standalone file.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since I moved getConcurrentEdits and applyEdits to a standalone file, I made both of them public to be called in other test files.

Copy link
Member

Choose a reason for hiding this comment

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

The tests in lit_tests/incrParse are still using these python files, so we can’t delete them just yet. If you want to run them locally, you can do so as described here. You just need to replace build by test (and I think you might need to build the compiler to get the FileCheck executable to verify the files, but maybe there’s some other way of getting it, eg. through Homebrew, I’m not sure)

https://github.com/apple/swift-syntax/blob/94959f0693610c3f51fc8823c2f38bb00b064c26/CONTRIBUTING.md?plain=1#L10-L21

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh, I see. Maybe I could translate those tests in IncrementalParsingTests later when I added some functionality related to them.

SourceEdit(offset: 7, length: 0, replacementLength: 1),
]

XCTAssert(diffs == expectedDiffs)
Copy link
Member

Choose a reason for hiding this comment

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

Actually, now that I think about it, it would make sense to move getConcurrentEdits to _SwiftSyntaxTestSupport and move all of your new tests in Tests/SwiftSyntaxTestSupportTests because they are really testing that this helper function actually works. You can then also move applyEdits to _SwiftSyntaxTestSupport and use it from your new tests.

@StevenWong12 StevenWong12 force-pushed the add_helper_function_for_getting_sourceedit branch from ebf0b32 to 374160e Compare May 17, 2023 05:58
@StevenWong12 StevenWong12 requested a review from ahoppen May 17, 2023 06:09
let s3 = applyEdits(expectedDiffs, concurrent: true, to: s1)

XCTAssertEqual(s3, "?12456??")
}
Copy link
Member

Choose a reason for hiding this comment

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

Could you add one more test case that has an insertion at the start and an insertion at the end. That would make sure that Collection.difference actually contains sequential edits instead of concurrent edits.

let base = "0123"
let new = "a0123b"

Copy link
Member

Choose a reason for hiding this comment

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

Sorry if I wasn’t clear here. What I was looking for is a test case just like the one you have in testDiffOfTwoStrings (which I think has a very good test structure), just with the inputs I gave you. So basically:

  public func testStringDifferenceReturnSequentialEdits() throws {
    let base = "0123"
    let new = "a0123bc"

    let diffs = getConcurrentEdits(from: base, to: new)
    XCTAssertEqual(diffs.edits, [
        SourceEdit(offset: 0, length: 0, replacementLength: 1),
        SourceEdit(offset: 4, length: 0, replacementLength: 2)
      ])

    let appliedDiffsBase = applyEdits(diffs.edits, concurrent: true, to: base)

    XCTAssertEqual(appliedDiffsBase, "?0123??")
  }

Which seems to pass, so that’s good ✅

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great. Already added this.

@StevenWong12 StevenWong12 force-pushed the add_helper_function_for_getting_sourceedit branch from 57f5176 to 3dfd374 Compare May 19, 2023 09:43
Add one more test case to test the result of String.difference return sequential edits
@StevenWong12 StevenWong12 force-pushed the add_helper_function_for_getting_sourceedit branch from 3dfd374 to 95093d4 Compare May 19, 2023 09:50
@StevenWong12 StevenWong12 requested a review from ahoppen May 19, 2023 09:52
@ahoppen
Copy link
Member

ahoppen commented May 19, 2023

I just tried running some random tests on the getConcurrentEdits and it looks like there are still some bugs in it.

For example base = "0a1ab" and new = "bb01d" seems to produce incorrect edits. The following test fails

let base = "0a1ab"
let new =  "bb01d"

let diffs = getConcurrentEdits(from: base, to: new)

let appliedDiffsBase = applyEdits(diffs.edits, concurrent: true, to: base)

XCTAssertEqual(appliedDiffsBase, "??aa?")

The automated test that I used to find this failure was the following (very hacky, the code itself is not ready to be submitted to the repo)

func randomString(length: Int) -> String {
  let letters = "abcd"
  return String((0..<length).map{ _ in letters.randomElement()! })
}

public func testFoo() {
  for i in 0..<100_000 {
    let startLength = Int.random(in: 0..<6)
    let endLength = Int.random(in: 0..<6)

    let base = randomString(length: startLength)
    let new = randomString(length: endLength)

    let edits = getConcurrentEdits(from: base, to: new)
    let editsApplied = applyEdits(edits.edits, concurrent: true, to: base)

    assert(new.count == editsApplied.count)
    for (n, e) in zip(new, editsApplied) {
      assert(e == "?" || n == e)
    }
  }
}

@StevenWong12
Copy link
Contributor Author

I found String.Difference sometimes produces some unexpected outputs. e.g.

"class".difference(from: "struct")

The function seems to center on the c in "struct" and view other characters as deletions and "lass" as insertions, which is not we expect.

I opened another draft pr to use markers to mark source edits and generate original and edited source (which is a bit like the previous one in the old parser), I think this should be easier to mark and test, also avoid being trapped in the underlying logic of String.Difference. Maybe we could close this pr and turn to that one. What do you think?

@ahoppen
Copy link
Member

ahoppen commented May 22, 2023

Thanks for catching that. With that explanation, I agree that being explicit about the edits makes a lot of sense. 👍🏽 So, I prefer #1684

@StevenWong12
Copy link
Contributor Author

Cool. Let's just close this and continue in the other pr.

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.

2 participants