Display line diffs in dsl --verify error messages#2592
Conversation
f3c5b51 to
94ecc33
Compare
| end.join("\n") | ||
|
|
||
| raise Tapioca::Error, <<~ERROR | ||
| file_diff = if diff_output.lines.count.between?(1, 250) |
There was a problem hiding this comment.
Should this 250 limit be a CLI option instead and default to 250? dsl --verify --max-diff-lines=1000
|
|
||
| stdout | ||
| rescue => e | ||
| say_error("Failed to create #{filename} diff. #{e.message}", :yellow) |
There was a problem hiding this comment.
Why did you opt for yellow and not red? I think red is more consistent with rest of our errors.
There was a problem hiding this comment.
I looked through say_error uses and it seemed like :yellow was used for warnings and :red for errors. I leaned towards this being a warning. Makes sense, changed to :red :)
| @command = DslVerify.new( | ||
| requested_constants: [], | ||
| requested_paths: [], | ||
| outpath: Pathname.new("tmp"), |
There was a problem hiding this comment.
Does DslVerify write something to the file system? I think other DslVerify tests run on a temporary directory that's cleaned up but this is not an integration test so it shouldn't use that, it's more costly.
If it is writing something we should clean up after, maybe we can create @tmp = Dir.mktmpdir in the before block, use that as the outpath and then clean it up in an after block.
There was a problem hiding this comment.
If im understanding correctly, outpath is required to create DslVerify but the command isn't being executed here, I just need an instance to pull file_diff from. I changed it to /dev/null to make it clearer that its unused. tbh I wouldn't be that against removing this test file if we don't find it helpful, file diff is being tested by verify tests also
There was a problem hiding this comment.
Actually ignore above, I moved out file_diff to rbi_files_helpers and the tests look much nicer now :)
0aa9400 to
6bdba8a
Compare
KaanOzkan
left a comment
There was a problem hiding this comment.
Sorry I'm suggesting a refactor that I didn't noticed before.
| old_path.to_s, | ||
| new_path.to_s, | ||
| ) | ||
| Kernel.raise stderr.chomp unless [0, 1].include?(status.exitstatus) |
There was a problem hiding this comment.
Using exceptions for control flow is frowned upon and slow. So we shouldn't rely on it when we can just call say_error here instead. If Open3 itself can raise while running keep the rescue block too.
| Kernel.raise stderr.chomp unless [0, 1].include?(status.exitstatus) | |
| say_error("Failed to create #{filename} diff. #{e.message}", :red) unless [0, 1].include?(status.exitstatus) |
There was a problem hiding this comment.
Using exceptions for control flow is frowned upon and slow
Noted, thank you !
| #: (Pathname dir) -> void | ||
| def perform_dsl_verification(dir) | ||
| diff = verify_dsl_rbi(tmp_dir: dir) | ||
| diff, diff_output = verify_dsl_rbi(tmp_dir: dir) |
There was a problem hiding this comment.
I don't love the tuple return here and I also forgot about report_diff_and_exit_if_out_of_date. What do you think about moving the calculation of diff output to report_diff_and_exit_if_out_of_date? It's the same class but it'll move some logic away from perform_dsl_verification/verify_dsl_rbi and make it easier to read. Something like this:
#: (Hash[String, Symbol] diff, tmp_dir: Pathname, command: Symbol) -> void
def report_diff_and_exit_if_out_of_date(diff, tmp_dir:, command:)
if diff.empty?
say("Nothing to do, all RBIs are up-to-date.")
return
end
reasons = diff.group_by(&:last).sort.map do |cause, diff_for_cause|
build_error_for_files(cause, diff_for_cause.map(&:first))
end.join("\n")
diff_output = build_diff_output(diff, tmp_dir)
diff_lines = diff_output.count("\n")
diff_section =
if diff_lines.between?(1, @max_diff_lines)
"#{set_color("Diff:", :red)}\n#{diff_output.chomp}"
elsif diff_lines > @max_diff_lines
set_color("Diff not displayed as it exceeds #{@max_diff_lines} lines", :red)
else
""
end
raise Tapioca::Error, <<~ERROR.rstrip
#{set_color("RBI files are out-of-date. In your development environment, please run:", :green)}
#{set_color("`#{default_command(command)}`", :green, :bold)}
#{set_color("Once it is complete, be sure to commit and push any changes", :green)}
If you don't observe any changes after running the command locally, ensure your database is in a good
state e.g. run `bin/rails db:reset`
#{set_color("Reason:", :red)}
#{reasons}
#{diff_section}
ERROR
end
#: (Hash[String, Symbol] diff, Pathname tmp_dir) -> String
def build_diff_output(diff, tmp_dir)
out = String.new
line_count = 0
diff.each do |file, status|
chunk = case status
when :added then file_diff(file, File::NULL, tmp_dir / file)
when :removed then file_diff(file, @outpath / file, File::NULL)
when :changed then file_diff(file, @outpath / file, tmp_dir / file)
end
out << chunk
line_count += chunk.count("\n")
break if line_count > @max_diff_lines
end
out
endThere was a problem hiding this comment.
Agree, changed. Thanks!
| def initialize(name) | ||
| Minitest::Runnable.instance_method(:initialize).bind(self).call(name) | ||
| end | ||
| end |
There was a problem hiding this comment.
Instead of this could we get away with defining our own say_error method? It's simpler if that's all we need.
There was a problem hiding this comment.
We could, but I believe we'd have to turn off type check in the file since files helpers require Thor as ancestor
There was a problem hiding this comment.
Hmm yeah I guess we could also extract file_diff to another file and remove the say_error dependency. Curious if anyone else has a better suggestion, worth asking on slack.
There was a problem hiding this comment.
extract file_diff to another file and remove the say_error dependency
Done:) I moved file_diff to its own file and replaced say_error with a warn
6bdba8a to
85ec455
Compare
| stdout, stderr, status = Open3.capture3( | ||
| "diff", | ||
| "-u", | ||
| "--label", | ||
| filename, | ||
| "--label", | ||
| filename, | ||
| old_path, | ||
| new_path, | ||
| ) |
There was a problem hiding this comment.
You can also use the = form to keep the --label flag name with its value, and you can interleave labels and file paths:
| stdout, stderr, status = Open3.capture3( | |
| "diff", | |
| "-u", | |
| "--label", | |
| filename, | |
| "--label", | |
| filename, | |
| old_path, | |
| new_path, | |
| ) | |
| stdout, stderr, status = Open3.capture3( | |
| "diff", | |
| "-u", | |
| "--label=\"Current #{filename}\"", | |
| old_path, | |
| "--label=\"Correct #{filename} (After running `bin/tapioca dsl`)\"", | |
| new_path, | |
| ) |
Example:
$ diff -u --label="before" <(echo "hello") --label "after" <(echo "world")
--- before
+++ after
@@ -1 +1 @@
-hello
+world| filename = file.to_s | ||
| old_path = (@outpath / file).to_s | ||
| new_path = (tmp_dir / file).to_s |
There was a problem hiding this comment.
Move the to_s calls into file_diff
| chunk = case status | ||
| when :added then file_diff(filename, File::NULL, new_path) | ||
| when :removed then file_diff(filename, old_path, File::NULL) | ||
| when :changed then file_diff(filename, old_path, new_path) |
There was a problem hiding this comment.
Just to confirm, does this ever call file_diff at most once per path? Or is it possible diff contains multiple entries pertaining to the same file?
There was a problem hiding this comment.
Yes, each file can only have one status (added, removed, or changed)
85ec455 to
70de51e
Compare
| end | ||
|
|
||
| stdout | ||
| rescue => e |
There was a problem hiding this comment.
What are we aiming to rescue here, exactly?
There was a problem hiding this comment.
I narrowed this to SystemCallError. The thought was that if for some reason we couldn't execute diff we'd continue, though maybe thats overkill?
|
|
||
| Diff: | ||
| --- Current post.rbi | ||
| +++ Correct post.rbi (After running `bin/tapioca dsl`) |
| - | ||
| -# DO NOT EDIT MANUALLY | ||
| -# This is an autogenerated file for dynamic methods in `Post`. | ||
| -# Please instead update this file by running `bin/tapioca dsl Post`. |
There was a problem hiding this comment.
Oooo can we reuse whatever logic is used to generate this bin/tapioca dsl Post, and use that in the +++ label above?
There was a problem hiding this comment.
This would be awesome! Currently we don't have easy access to the constant by the time we're here, but I'll work on a separate PR for this
| --- Current post.rbi | ||
| +++ Correct post.rbi (After running `bin/tapioca dsl`) | ||
| @@ -10,6 +10,12 @@ | ||
| #{" "} |
There was a problem hiding this comment.
intentional spaces? Kinda weird
There was a problem hiding this comment.
Kinda weird
Agree, diff -u uses single space as the "unchanged" marker
| Dir.mktmpdir do |dir| | ||
| a = File.join(dir, "a") | ||
| b = File.join(dir, "b") | ||
| File.write(a, "line1\nline2\n") | ||
| File.write(b, "line1\nline3\n") |
There was a problem hiding this comment.
| Dir.mktmpdir do |dir| | |
| a = File.join(dir, "a") | |
| b = File.join(dir, "b") | |
| File.write(a, "line1\nline2\n") | |
| File.write(b, "line1\nline3\n") | |
| Pathname.mktmpdir do |dir| | |
| a = dir / "a" | |
| b = dir / "b" | |
| a.write("line1\nline2\n") | |
| b.write("line1\nline3\n") |
There was a problem hiding this comment.
It seems like we don't get Pathname.mktmpdir until Ruby 3.4 and gemspec min is 3.2, but I just wrapped it
70de51e to
abe279f
Compare
| "#{set_color("Diff:", :red)}\n#{diff_output.chomp}" | ||
| elsif diff_lines > @max_diff_lines | ||
| truncated_output = diff_output.lines.first(@max_diff_lines).join | ||
| "#{set_color("Diff truncated to #{@max_diff_lines} lines:", :red)}\n#{truncated_output.rstrip}" |
There was a problem hiding this comment.
Could mention the new CLI flag here.
| halt_upon_load_error: options[:halt_upon_load_error], | ||
| compiler_options: options[:compiler_options], | ||
| lsp_addon: self.class.addon_mode, | ||
| max_diff_lines: options[:max_diff_lines], |
There was a problem hiding this comment.
We should check that it's a positive number just in case. It'd error later on anyways but I think it's good to validate user input at this layer.
abe279f to
384c457
Compare
384c457 to
1d85148
Compare
| assert_includes(result, "--- Current x.rbi") | ||
| assert_includes(result, "+++ Correct x.rbi (After running `bin/tapioca dsl`)") | ||
| assert_includes(result, "-line2") | ||
| assert_includes(result, "+line3") |
There was a problem hiding this comment.
Any reason not to assert over the entire output? That'll ensure the order of these snippets is correct
| line_count += chunk.count("\n") | ||
| break if line_count > @max_diff_lines |
There was a problem hiding this comment.
Minor bug: this can permit too many lines. Imagine the limit is 250, and there's 3 chunks of 200 each.
The first one will be let through, but so would the second (without truncation). The line_count will hit 400, so we would break before the third chunk get's concatenated in, but we won't trim off the 150 excess lines of the second chunk.
There was a problem hiding this comment.
Truncation and the this was truncated message output were happening in report_diff. The intention with break if line_count > @max_diff_lines was just to not loop longer than necessary
I can see how that was odd/confusing tho, so I've moved truncation into build_diff and now return both the output and whether it was truncated
1d85148 to
7ebcc35
Compare
When `dsl --verify` detects out of date RBI files, the error message lists which files were added, removed, or changed. We could make it easier to understand what has changed by including the line diff output in the error message as well This commit grabs the line diff output during RBI verification and includes it in the error message when the diff is 250 lines or less
7ebcc35 to
b7963a2
Compare
| "-u", | ||
| "--label=Current #{filename}", | ||
| old_path.to_s, | ||
| "--label=Correct #{filename} (After running `bin/tapioca dsl`)", |
There was a problem hiding this comment.
| "--label=Correct #{filename} (After running `bin/tapioca dsl`)", | |
| "--label=Expected #{filename} (After running `bin/tapioca dsl`)", |
|
|
||
| unless [0, 1].include?(status.exitstatus) | ||
| error_msg("Failed to create #{filename} diff. #{stderr.chomp}") | ||
| return "" |
There was a problem hiding this comment.
I'd return nil if it couldn't be computed

Motivation
When
tapioca dsl --verifydetects out of date RBI files, the error message lists which files were added, removed, or changed. We could make it easier to visualize these changes by including the line diff output in the error messageOutput example
Implementation
This PR diffs updated files against the originals during RBI verification and then includes the output in the error message when the diff is 250 lines or less, or whatever value is provided to
--max-diff-linesflagTests
verifytests to include line diff