From bc0662064896e701a9f21d762ab9440177149fa6 Mon Sep 17 00:00:00 2001 From: kbkpbot Date: Fri, 16 May 2025 13:14:58 +0800 Subject: [PATCH] v.util: use internal diff (#24495) --- vlib/v/fmt/fmt_vlib_test.v | 1 - vlib/v/gen/js/program_test.v | 6 - vlib/v/slow_tests/inout/compiler_test.v | 1 - vlib/v/util/diff/diff.v | 236 +++++------------------- vlib/v/util/diff/diff_test.v | 85 +-------- 5 files changed, 50 insertions(+), 279 deletions(-) diff --git a/vlib/v/fmt/fmt_vlib_test.v b/vlib/v/fmt/fmt_vlib_test.v index abe657d5a..3e3f8cf72 100644 --- a/vlib/v/fmt/fmt_vlib_test.v +++ b/vlib/v/fmt/fmt_vlib_test.v @@ -29,7 +29,6 @@ fn test_vlib_fmt() { } vroot := os.dir(vexe) tmpfolder := os.temp_dir() - diff_cmd := diff.find_working_diff_command() or { '' } mut fmt_bench := benchmark.new_benchmark() os.chdir(vroot) or {} input_files := os.walk_ext('vlib/v/', '.v').filter(!it.contains('/tests/')) diff --git a/vlib/v/gen/js/program_test.v b/vlib/v/gen/js/program_test.v index b3a0d2619..784fb5690 100644 --- a/vlib/v/gen/js/program_test.v +++ b/vlib/v/gen/js/program_test.v @@ -7,14 +7,8 @@ const vexe = @VEXE const vroot = @VMODROOT -const diff_cmd = find_diff_cmd() - const github_job = os.getenv('GITHUB_JOB') -fn find_diff_cmd() string { - return diff.find_working_diff_command() or { '' } -} - @[noreturn] fn exit_because(msg string) { eprintln('${msg}, tests will not run') diff --git a/vlib/v/slow_tests/inout/compiler_test.v b/vlib/v/slow_tests/inout/compiler_test.v index aece072a3..2139b5555 100644 --- a/vlib/v/slow_tests/inout/compiler_test.v +++ b/vlib/v/slow_tests/inout/compiler_test.v @@ -23,7 +23,6 @@ fn test_all() { vexe := os.getenv('VEXE') vroot := os.dir(vexe) os.chdir(vroot) or {} - diff_cmd := diff.find_working_diff_command() or { '' } dir := 'vlib/v/slow_tests/inout' mut files := os.ls(dir) or { panic(err) } files.sort() diff --git a/vlib/v/util/diff/diff.v b/vlib/v/util/diff/diff.v index c49e7f6b8..633bfab7a 100644 --- a/vlib/v/util/diff/diff.v +++ b/vlib/v/util/diff/diff.v @@ -1,8 +1,36 @@ -@[has_globals] module diff import os -import time +// import term +import arrays.diff as arrays_diff + +// compare_files returns a string displaying the differences between two files. +pub fn compare_files(path1 string, path2 string, _ CompareOptions) !string { + src := os.read_lines(path1)! + dst := os.read_lines(path2)! + mut ctx := arrays_diff.diff(src, dst) + patch := ctx.generate_patch( + colorful: true // term.can_show_color_on_stdout() + block_header: true + unified: 3 + ) + return patch +} + +// compare_text returns a string displaying the differences between two strings. +pub fn compare_text(text1 string, text2 string, _ CompareTextOptions) !string { + src := text1.split_into_lines() + dst := text2.split_into_lines() + mut ctx := arrays_diff.diff(src, dst) + patch := ctx.generate_patch( + colorful: true // term.can_show_color_on_stdout() + block_header: true + unified: 3 + ) + return patch +} + +// deprecated code : pub enum DiffTool { auto @@ -15,215 +43,45 @@ pub enum DiffTool { @[params] pub struct CompareOptions { pub: - tool DiffTool + tool DiffTool @[deprecated_after: '2025-12-31'] // Custom args used with the diff command. - args string + args string @[deprecated_after: '2025-12-31'] // 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' + env_overwrite_var ?string = 'VDIFF_CMD' @[deprecated_after: '2025-12-31'] } @[params] pub struct CompareTextOptions { CompareOptions pub: - base_name string = 'base' - target_name string = 'target' + base_name string = 'base' @[deprecated_after: '2025-12-31'] + target_name string = 'target' @[deprecated_after: '2025-12-31'] } -// 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' -} - -// List of detected diff tools. -__global cache_of_available_tools = []DiffTool{} - // Allows public checking for the available tools and prevents repeated searches // when using compare functions with automatic diff tool detection. +@[deprecated_after: '2025-12-31'] pub fn available_tools() []DiffTool { - if cache_of_available_tools.len == 0 { - cache_of_available_tools = find_working_diff_tools() - } - return cache_of_available_tools -} - -// 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 := 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 CompareTextOptions) !string { - 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, opts.base_name) - path2 := os.join_path_single(tmp_dir, opts.target_name) - // 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.CompareOptions)! -} - -fn (opts CompareOptions) find_tool() !(DiffTool, string) { - tool := if opts.tool == .auto { - auto_tool := available_tools()[0] or { - return error('error: failed to find comparison command') - } - - auto_tool - } else { - opts.tool - } - cmd := tool.cmd() - 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 -} - -// Returns a list of programmatically-compatible known diff tools. Its result is intended to be stored -// in a constant to prevent repeated searches when compare functions with automatic diff tool detection -// are used. Using a public constant will also allow for external checking of available tools. -fn find_working_diff_tools() []DiffTool { - mut tools := []DiffTool{} - for tool in known_diff_tool_defaults.keys() { - cmd := tool.cmd() - 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 - } - } - tools << tool - } - return tools -} - -fn (dt DiffTool) cmd() string { - cmd := dt.str() - return $if windows { '${cmd}.exe' } $else { cmd } -} - -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') + return [] } +@[deprecated_after: '2025-12-31'] pub fn find_working_diff_command() !string { - env_difftool := os.getenv('VDIFF_TOOL') - env_diffopts := os.getenv('VDIFF_OPTIONS') - if env_difftool != '' { - os.find_abs_path_of_executable(env_difftool) or { - return error('could not find specified VDIFF_TOOL `${env_difftool}`') - } - return '${env_difftool} ${env_diffopts}' - } - known_diff_tools := ['colordiff', 'gdiff', 'diff', 'colordiff.exe', 'diff.exe', 'opendiff', - 'code', 'code.cmd'] // NOTE: code.cmd is the Windows variant of the `code` cli tool - mut diff_cmd := '' - for cmd in known_diff_tools { - os.find_abs_path_of_executable(cmd) or { continue } - diff_cmd = cmd - break - } - if diff_cmd == '' { - return error('No working "diff" command found') - } - if diff_cmd in ['code', 'code.cmd'] { - // Make sure the diff flag `-d` is included in any case. - return '${diff_cmd} ${env_diffopts} -d' - } - // Don't add spaces to the cmd if there are no `env_diffopts`. - return if env_diffopts != '' { '${diff_cmd} ${env_diffopts}' } else { diff_cmd } + return error('deprecated') } // color_compare_files returns a colored diff between two files. -pub fn color_compare_files(diff_cmd string, path1 string, path2 string) string { - 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 ${default_diff_args} ${p1} ${p2}') - if !res.output.starts_with('diff: unrecognized option') { - return res.output.trim_right('\r\n') - } - } - cmd := '${diff_cmd} ${default_diff_args} ${p1} ${p2}' - return os.execute(cmd).output.trim_right('\r\n') +@[deprecated: 'use compare_files instead'] +@[deprecated_after: '2025-12-31'] +pub fn color_compare_files(_ string, path1 string, path2 string) string { + return compare_files(path1, path2) or { '' } } // color_compare_strings returns a colored diff between two strings. -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 {} - defer { - os.rmdir_all(tmp_dir) or {} - } - ctime := time.sys_mono_now() - e_file := os.join_path_single(tmp_dir, '${ctime}.expected.txt') - f_file := os.join_path_single(tmp_dir, '${ctime}.found.txt') - os.write_file(e_file, expected) or { panic(err) } - os.write_file(f_file, found) or { panic(err) } - 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}') +@[deprecated: 'use compare_text instead'] +@[deprecated_after: '2025-12-31'] +pub fn color_compare_strings(_ string, _ string, expected string, found string) string { + return compare_text(expected, found) or { '' } } diff --git a/vlib/v/util/diff/diff_test.v b/vlib/v/util/diff/diff_test.v index e6b135c6c..ed43b7d3a 100644 --- a/vlib/v/util/diff/diff_test.v +++ b/vlib/v/util/diff/diff_test.v @@ -5,18 +5,7 @@ import term const tdir = os.join_path(os.vtmp_dir(), 'diff_test') fn testsuite_begin() { - if diff.DiffTool.diff !in diff.available_tools() { - // On GitHub runners, diff should be available on all platforms. - // Prevent regressions by failing instead of skipping when it's not detected. - if os.getenv('CI') == 'true' { - exit(1) - } - eprintln('> skipping test `${@FILE}`, since this test requires `diff` to be installed') - exit(0) - } os.mkdir_all(tdir)! - // Disable environmental overwrites that can result in different compare outputs. - os.setenv('VDIFF_CMD', '', true) } fn testsuite_end() { @@ -44,89 +33,21 @@ fn test_compare_files() { os.write_file(p1, f1)! os.write_file(p2, f2)! - // 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 - assert res == diff.color_compare_files('diff', p1, p2) - // Test again using `find_working_diff_command()`. - zzz := diff.color_compare_files(diff.find_working_diff_command()!, p1, p2) - assert term.strip_ansi(res) == term.strip_ansi(zzz) - - // 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 - assert res == term.strip_ansi(diff.color_compare_files('diff --ignore-case', 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("+\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 - 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)! + mut res := diff.compare_text('abc', 'abcd')! + println(res) assert res.contains('-abc'), res assert res.contains('+abcd'), res assert !res.contains('No newline at end of file'), res - // Default base and target name. - assert res.match_glob('*---*base*'), res - assert res.match_glob('*+++*target*'), res - // Custom base and target name. - res = diff.compare_text('abc', 'abcd', tool: .diff, base_name: 'old.v', target_name: 'new.v')! - assert res.match_glob('*---*old.v*'), res - assert res.match_glob('*+++*new.v*'), res -} - -fn test_coloring() { - if os.execute('diff --color=always').output.starts_with('diff: unrecognized option') { - eprintln('> skipping test `${@FN}`, since `diff` does not support --color=always') - return - } - f1 := 'abc\n' - f2 := 'abcd\n' - p1 := os.join_path(tdir, '${@FN}_f1.txt') - p2 := os.join_path(tdir, '${@FN}_f2.txt') - os.write_file(p1, f1)! - os.write_file(p2, f2)! - 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 - assert res == diff.color_compare_files('diff', p1, p2) } -- 2.39.5