Skip to content

Commit

Permalink
v.util: rewrite diff module, deprecate old functions (#21403)
Browse files Browse the repository at this point in the history
  • Loading branch information
ttytm committed May 5, 2024
1 parent 8e7a6e3 commit 97984ad
Show file tree
Hide file tree
Showing 4 changed files with 221 additions and 51 deletions.
20 changes: 1 addition & 19 deletions cmd/tools/vfmt.v
Original file line number Diff line number Diff line change
Expand Up @@ -200,19 +200,6 @@ fn print_compiler_options(compiler_params &pref.Preferences) {
eprintln(' is_script: ${compiler_params.is_script} ')
}

fn (mut foptions FormatOptions) find_diff_cmd() string {
if foptions.diff_cmd != '' {
return foptions.diff_cmd
}
if foptions.is_verify || foptions.is_diff {
foptions.diff_cmd = diff.find_working_diff_command() or {
eprintln(err)
exit(1)
}
}
return foptions.diff_cmd
}

fn (mut foptions FormatOptions) post_process_file(file string, formatted_file_path string) ! {
if formatted_file_path == '' {
return
Expand All @@ -230,12 +217,7 @@ fn (mut foptions FormatOptions) post_process_file(file string, formatted_file_pa
if !is_formatted_different {
return
}
diff_cmd := foptions.find_diff_cmd()
foptions.vlog('Using diff command: ${diff_cmd}')
diff_ := diff.color_compare_files(diff_cmd, file, formatted_file_path)
if diff_.len > 0 {
println(diff_)
}
println(diff.compare_files(file, formatted_file_path)!)
return
}
if foptions.is_verify {
Expand Down
6 changes: 6 additions & 0 deletions vlib/v/util/diff.v
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,22 @@ module util
import v.util.diff

// find_working_diff_command returns the first available command from a list of known diff cli tools.
@[deprecated_after: '2024-06-30']
@[deprecated]
pub fn find_working_diff_command() !string {
return diff.find_working_diff_command()
}

// color_compare_files returns a colored diff between two files.
@[deprecated: 'use `diff.compare_files` instead']
@[deprecated_after: '2024-06-30']
pub fn color_compare_files(diff_cmd string, path1 string, path2 string) string {
return diff.color_compare_files(diff_cmd, path1, path2)
}

// color_compare_strings returns a colored diff between two strings.
@[deprecated: 'use `diff.compare_text` instead']
@[deprecated_after: '2024-06-30']
pub fn color_compare_strings(diff_cmd string, unique_prefix string, expected string, found string) string {
return diff.color_compare_strings(diff_cmd, unique_prefix, expected, found)
}
175 changes: 159 additions & 16 deletions vlib/v/util/diff/diff.v
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,146 @@ module diff
import os
import time

pub enum DiffTool {
auto
diff // core package on Unix-like systems.
colordiff // `diff` wrapper.
delta // viewer for git and diff output.
// fc // built-in tool on windows. // TODO: enable when its command output can be read.
}

@[params]
pub struct CompareOptions {
pub:
tool DiffTool
// Custom args used with the diff command.
args string
// Sets the environment variable whose value can overwrite a diff command passed to a compare function.
// It also enables the use of commands that are not in the list of known diff tools.
// Set it to `none` to disable it.
env_overwrite_var ?string = 'VDIFF_CMD'
}

// Default options for `diff` and `colordiff`.
// Short `diff` args are supported more widely (e.g. on OpenBSD, ref. https://man.openbsd.org/diff.1).
// `-d -a -U 2` ^= `--minimal --text --unified=2`
const default_diff_args = $if openbsd || freebsd { '-d -a -U 2' } $else { '-d -a -U 2 -F "fn "' }
const known_diff_tool_defaults = {
// When searching for an automatically available diff tool, the tools are searched in this order.
DiffTool.delta: ''
.colordiff: default_diff_args
.diff: default_diff_args
// .fc: '/lnt'
}

// Allows public checking for the available tool and prevents repeated searches
// when using compare functions with automatic diff tool detection.
pub const available_tool = find_working_diff_tool()

// compare_files returns a string displaying the differences between two files.
pub fn compare_files(path1 string, path2 string, opts CompareOptions) !string {
p1, p2 := os.quoted_path(os.real_path(path1)), os.quoted_path(os.real_path(path2))
if v := opts.env_overwrite_var {
env_cmd := os.getenv(v)
if env_cmd != '' {
tool, args := env_cmd.split_once(' ') or { env_cmd, opts.args }
os.find_abs_path_of_executable(tool) or {
return error('error: failed to find comparison command `${tool}`')
}
return run_tool('${tool} ${args} ${p1} ${p2}', @LOCATION)
}
}
tool, cmd := opts.find_tool()!
mut args := opts.args
if args == '' {
args = if defaults := diff.known_diff_tool_defaults[tool] { defaults } else { '' }
if opts.tool == .diff {
// Ensure that the diff command supports the color option.
// E.g., some BSD installations or macOS diff (based on FreeBSD diff)
// might not include additional diffutils by default.
res := run_tool('${cmd} ${args} --color=always ${p1} ${p2}', @LOCATION)
if !res.contains('unrecognized option') {
return res
}
}
}
return run_tool('${cmd} ${args} ${p1} ${p2}', @LOCATION)
}

// compare_text returns a string displaying the differences between two strings.
pub fn compare_text(text1 string, text2 string, opts CompareOptions) !string {
opts.find_tool()!
if text1 == text2 {
return ''
}
ctime := time.sys_mono_now()
tmp_dir := os.join_path_single(os.vtmp_dir(), ctime.str())
os.mkdir(tmp_dir)!
defer {
os.rmdir_all(tmp_dir) or {}
}
path1 := os.join_path_single(tmp_dir, 'text1.txt')
path2 := os.join_path_single(tmp_dir, 'text2.txt')
// When comparing strings and not files, prevent `\ No newline at end of file` in the output.
if !text1.ends_with('\n') || !text2.ends_with('\n') {
os.write_file(path1, text1 + '\n')!
os.write_file(path2, text2 + '\n')!
} else {
os.write_file(path1, text1)!
os.write_file(path2, text2)!
}
return compare_files(path1, path2, opts)!
}

fn (opts CompareOptions) find_tool() !(DiffTool, string) {
tool := if opts.tool == .auto {
auto_tool := diff.available_tool or {
return error('error: failed to find comparison command')
}

auto_tool
} else {
opts.tool
}
cmd := $if windows { '${tool.str()}.exe' } $else { tool.str() }
if opts.tool == .auto {
// At this point it was already ensured that the automatically detected tool is available.
return tool, cmd
}
os.find_abs_path_of_executable(cmd) or {
return error('error: failed to find comparison command `${cmd}`')
}
return tool, cmd
}

fn find_working_diff_tool() ?DiffTool {
for tool in diff.known_diff_tool_defaults.keys() {
cmd := $if windows { '${tool.str()}.exe' } $else { tool.str() }
os.find_abs_path_of_executable(cmd) or { continue }
if tool == .delta {
// Sanity check that the `delta` executable is actually the diff tool.
res := os.execute_opt('${cmd} --help') or { continue }
help_desc := res.output.trim_space().all_before('\n')
if !help_desc.contains('diff') {
dbg('delta does not appear to be the diff tool `${help_desc}`', @LOCATION)
continue
}
}
return tool
}
return none
}

fn run_tool(cmd string, dbg_location string) string {
dbg('cmd=`${cmd}`', dbg_location)
res := os.execute(cmd)
dbg('res=`${res}`', dbg_location)
return res.output.trim_right('\r\n')
}

// find_working_diff_command returns the first available command from a list of known diff cli tools.
@[deprecated_after: '2024-06-30']
@[deprecated]
pub fn find_working_diff_command() !string {
env_difftool := os.getenv('VDIFF_TOOL')
env_diffopts := os.getenv('VDIFF_OPTIONS')
Expand Down Expand Up @@ -33,28 +172,27 @@ pub fn find_working_diff_command() !string {
}

// color_compare_files returns a colored diff between two files.
@[deprecated: 'use `compare_files` instead']
@[deprecated_after: '2024-06-30']
pub fn color_compare_files(diff_cmd string, path1 string, path2 string) string {
cmd := diff_cmd.all_before(' ')
os.find_abs_path_of_executable(cmd) or { return 'comparison command: `${cmd}` not found' }
flags := $if openbsd {
['-d', '-a', '-U', '2']
} $else $if freebsd {
['--minimal', '--text', '--unified=2']
} $else {
['--minimal', '--text', '--unified=2', '--show-function-line="fn "']
}
if cmd == 'diff' {
color_diff_cmd := '${diff_cmd} --color=always ${flags.join(' ')} ${os.quoted_path(path1)} ${os.quoted_path(path2)}'
color_result := os.execute(color_diff_cmd)
if !color_result.output.starts_with('diff: unrecognized option') {
return color_result.output.trim_right('\r\n')
tool := diff_cmd.all_before(' ')
os.find_abs_path_of_executable(tool) or { return 'comparison command: `${tool}` not found' }
p1, p2 := os.quoted_path(os.real_path(path1)), os.quoted_path(os.real_path(path2))
if tool == 'diff' {
// Ensure that the diff command supports the color option.
// E.g., some BSD installations do not include `diffutils` as a core package alongside `diff`.
res := os.execute('${diff_cmd} --color=always ${diff.default_diff_args} ${p1} ${p2}')
if !res.output.starts_with('diff: unrecognized option') {
return res.output.trim_right('\r\n')
}
}
full_cmd := '${diff_cmd} ${flags.join(' ')} ${os.quoted_path(path1)} ${os.quoted_path(path2)}'
return os.execute(full_cmd).output.trim_right('\r\n')
cmd := '${diff_cmd} ${diff.default_diff_args} ${p1} ${p2}'
return os.execute(cmd).output.trim_right('\r\n')
}

// color_compare_strings returns a colored diff between two strings.
@[deprecated: 'use `compare_text` instead']
@[deprecated_after: '2024-06-30']
pub fn color_compare_strings(diff_cmd string, unique_prefix string, expected string, found string) string {
tmp_dir := os.join_path_single(os.vtmp_dir(), unique_prefix)
os.mkdir(tmp_dir) or {}
Expand All @@ -69,3 +207,8 @@ pub fn color_compare_strings(diff_cmd string, unique_prefix string, expected str
res := color_compare_files(diff_cmd, e_file, f_file)
return res
}

@[if vdiff_debug ?]
fn dbg(msg string, location string) {
println('[DIFF DEBUG] ${location}: ${msg}')
}
71 changes: 55 additions & 16 deletions vlib/v/util/diff/diff_test.v
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import v.util.diff
import os
import term

const tdir = os.join_path(os.vtmp_dir(), 'diff_test')

fn testsuite_begin() {
// Disable environmental overwrites that can result in different compare outputs.
os.setenv('VDIFF_CMD', '', true)
os.find_abs_path_of_executable('diff') or {
eprintln('> skipping test, since this test requires `diff` to be installed')
eprintln('> skipping test `${@FILE}`, since this test requires `diff` to be installed')
exit(0)
}
os.mkdir_all(tdir)!
Expand Down Expand Up @@ -36,39 +39,73 @@ fn test_compare_files() {
os.write_file(p1, f1)!
os.write_file(p2, f2)!

mut res := diff.color_compare_files('diff', p1, p2)
// Test comparison without specifying a cmd only loosely, since an automatically detected tool
// or can result in a different compare output.
mut res := term.strip_ansi(diff.compare_files(p1, p2)!)
assert res.contains("name: 'Foo'"), res
assert res.contains("name: 'foo'"), res

// From here on, pass `.diff` via the arg or environment variable to enforce consistent behavior in regular tests.
res = diff.compare_files(p1, p2, tool: .diff)!
assert res.contains("-\tname: 'Foo'"), res
assert res.contains("+\tname: 'foo'"), res
assert res.contains("-\tversion: '0.0.0'"), res
assert res.contains("+\tversion: '0.1.0'"), res
assert res.contains("+\tlicense: 'MIT'"), res
// Test deprecated
assert res == diff.color_compare_files('diff', p1, p2)
// Test again using `find_working_diff_command()`.
assert res == diff.color_compare_files(diff.find_working_diff_command()!, p1, p2)

// Test adding a flag to the command.
res = diff.color_compare_files('diff --ignore-case', p1, p2)
// Test custom options.
res = diff.compare_files(p1, p2, tool: .diff, args: '-U 2 -i')!
assert !res.contains("+\tname: 'foo'"), res
assert res.contains("-\tversion: '0.0.0'"), res
assert res.contains("+\tversion: '0.1.0'"), res
assert res.contains("+\tlicense: 'MIT'"), res
// Test deprecated
assert res == term.strip_ansi(diff.color_compare_files('diff --ignore-case', p1, p2))

// Test again using `find_working_diff_command()`.
os.setenv('VDIFF_TOOL', 'diff', true)
res = diff.color_compare_files(diff.find_working_diff_command()!, p1, p2)
assert res.contains("-\tversion: '0.0.0'"), res
assert res.contains("+\tversion: '0.1.0'"), res
assert res.contains("+\tlicense: 'MIT'"), res

// Test adding a flag via env flag.
os.setenv('VDIFF_OPTIONS', '--ignore-case', true) // ignored, when VDIFF_TOOL is not explicitly set
res = diff.color_compare_files(diff.find_working_diff_command()!, p1, p2)
// Test options via env variable.
os.setenv('VDIFF_CMD', 'diff --ignore-case -U 2', true)
defer {
os.setenv('VDIFF_CMD', '', true)
}
res = diff.compare_files(p1, p2)!
assert !res.contains("+\tname: 'foo'"), res
assert res.contains("-\tversion: '0.0.0'"), res
assert res.contains("+\tversion: '0.1.0'"), res
assert res.contains("+\tlicense: 'MIT'"), res
// Test deprecated
os.setenv('VDIFF_TOOL', 'diff', true)
os.setenv('VDIFF_OPTIONS', '--ignore-case', true)
assert res == term.strip_ansi(diff.color_compare_files(diff.find_working_diff_command()!,
p1, p2))

// Test custom option that interferes with default options.
res = diff.compare_files(p1, p2, tool: .diff, args: '--side-by-side', env_overwrite_var: none)!
assert res.match_glob("*version: '0.0.0'*|*version: '0.1.0'*"), res

// Test custom diff command.
// Test windows default `fc`.
/* $if windows { // TODO: enable when its `os.execute` output can be read.
res = diff.compare_files(p1, p1, tool: .fc)!
assert res.contains('FC: no differences encountered')
res = diff.compare_files(p1, p2, tool: .fc, args: '/b')!
assert res.contains('FC: ABCD longer than abc')
} */
}

fn test_compare_string() {
mut res := diff.compare_text('abc', 'abcd', tool: .diff)!
assert res.contains('-abc'), res
assert res.contains('+abcd'), res
assert !res.contains('No newline at end of file'), res
}

fn test_coloring() {
if os.execute('diff --color=always').output.starts_with('diff: unrecognized option') {
eprintln('> skipping test, since `diff` does not support --color=always')
eprintln('> skipping test `${@FN}`, since `diff` does not support --color=always')
return
}
f1 := 'abc\n'
Expand All @@ -77,8 +114,10 @@ fn test_coloring() {
p2 := os.join_path(tdir, '${@FN}_f2.txt')
os.write_file(p1, f1)!
os.write_file(p2, f2)!
res := diff.color_compare_files('diff', p1, p2)
esc := rune(27)
res := diff.compare_files(p1, p2, tool: .diff)!
assert res.contains('${esc}[31m-abc${esc}['), res
assert res.contains('${esc}[32m+abcd${esc}['), res
// Test deprecated
assert res == diff.color_compare_files('diff', p1, p2)
}

0 comments on commit 97984ad

Please sign in to comment.