Files
ruby/tool/test/test_mkdepend.rb
Hiroshi SHIBATAandClaude Opus 5 58b42841dd Resolve mkdepend source names against the source root
A relative name was expanded against the current directory first, so a
build directory nested inside the source tree made generated files such
as ext/ripper/ripper.y look like source-tree files, and the extension's
own depend file was no longer found.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-29 14:05:32 +09:00

1287 lines
41 KiB
Ruby

require 'test/unit'
require 'tmpdir'
require 'stringio'
require_relative '../../lib/mkmf/depend'
TOP_SRCDIR = File.expand_path("../..", __dir__)
TestDepend = Class.new(MakeMakefile::Depend) do
public(*MakeMakefile::Depend.private_instance_methods(false))
class << self
public :parse_options
end
end
class TestMkdepend < Test::Unit::TestCase
MARK_START = MakeMakefile::Depend.const_get(:MARK_START, false)
MARK_END = MakeMakefile::Depend.const_get(:MARK_END, false)
Scanner = MakeMakefile::Depend.const_get(:Scanner, false)
def mkdepend
@mkdepend ||= TestDepend.new
end
def test_parse_options
options, inputs = TestDepend.parse_options(%w[
--root=src THREAD_MODEL=pthread --thread-model=win32 --output=.deps
--scope=extensions --nmake --sources --verbose depend
])
assert_equal(
{
root: "src",
make_variables: {"THREAD_MODEL" => "win32"},
output: ".deps",
mode: :output,
scope: :extensions,
nmake: true,
sources: true,
verbose: true,
},
options,
)
assert_equal(["depend"], inputs)
end
def test_parse_options_accepts_make_variable_assignments
options, inputs = TestDepend.parse_options(
["LOCAL_HDRS=", "--thread-model=win32", "THREAD_MODEL=pthread", "depend"],
)
assert_equal(
{
make_variables: {
"LOCAL_HDRS" => "",
"THREAD_MODEL" => "pthread",
},
},
options,
)
assert_equal(["depend"], inputs)
end
def test_parse_options_rejects_empty_thread_model
assert_raise(OptionParser::InvalidArgument) do
TestDepend.parse_options(["--thread-model="])
end
end
def test_parse_options_rejects_unknown_options
assert_raise(OptionParser::InvalidOption) do
TestDepend.parse_options(["--unknown"])
end
end
def test_parse_options_rejects_unknown_scope
assert_raise(OptionParser::InvalidArgument) do
TestDepend.parse_options(["--scope=unknown"])
end
end
def test_parse_options_selects_update_mode
assert_equal(
{mode: :inplace},
TestDepend.parse_options(["--inplace"]).first,
)
assert_equal(
{mode: :check},
TestDepend.parse_options(["--check"]).first,
)
end
def test_parse_options_uses_last_update_mode
options, = TestDepend.parse_options(["--output=.deps", "--check"])
assert_equal({mode: :check}, options)
options, = TestDepend.parse_options(["--inplace", "--output=.deps"])
assert_equal({mode: :output, output: ".deps"}, options)
end
def test_help_explains_update_mode_precedence
stdout, $stdout = $stdout, StringIO.new
exit_status = assert_raise(SystemExit) do
TestDepend.parse_options(["--help"])
end
assert_true(exit_status.success?)
help = $stdout.string
assert_include(help, "[VAR=value ...]")
assert_include(help, "same as THREAD_MODEL=MODEL")
assert_include(
help,
"Update mode (last one wins; default: stdout):",
)
output = help.index("--output=DIR")
inplace = help.index("--inplace")
check = help.index("--check")
assert_operator(output, :<, inplace)
assert_operator(inplace, :<, check)
ensure
$stdout = stdout
end
def test_scanner_follows_all_possible_project_headers
Dir.mktmpdir('mkdepend-scanner') do |dir|
File.write(File.join(dir, 'main.c'), <<~C)
#include "local.h"
#if FEATURE
# include "left.h"
#else
# include "right.h"
#endif
#if 0 && FEATURE
# include "never.h"
#endif
#include GENERATED_HEADER
#include "generated.rbinc"
#include "aliased.h"
#include "path/parse.h"
#ifdef DISABLED
# include "never_disabled.h"
#endif
#ifndef ENABLED
# include "never_enabled.h"
#endif
#include <external.h>
C
File.binwrite(File.join(dir, 'local.h'),
"/* \xff */\n#include \"nested.h\"\n".b)
%w[left.h right.h never.h never_disabled.h never_enabled.h nested.h].each do |header|
File.write(File.join(dir, header), "")
end
File.write(File.join(dir, 'aliased.h.erb'), "#include \"nested.h\"\n")
Dir.mkdir(File.join(dir, 'path'))
File.write(File.join(dir, 'path/parse.h'), "#include \"../path_nested.h\"\n")
File.write(File.join(dir, 'path_nested.h'), "")
scanner = Scanner.new(
root: dir,
include_dirs: [dir],
macros: {'GENERATED_HEADER' => 'generated.h'},
targets: %w[parse.h],
aliases: {'aliased.h' => 'aliased.h.erb'},
dependencies: {'generated.h' => %w[virtual.h]},
defined: %w[ENABLED],
undefined: %w[DISABLED],
)
assert_equal(
%w[aliased.h generated.rbinc left.h local.h main.c nested.h path/parse.h path_nested.h right.h virtual.h],
scanner.scan('main.c'),
)
end
end
def test_scanner_prefers_quoted_path
Dir.mktmpdir('mkdepend-scanner') do |dir|
root = File.join(dir, 'src')
source_dir = File.join(root, 'ext/-test-/float')
missing_dir = File.join(root, 'missing')
outer_missing_dir = File.join(dir, 'missing')
[source_dir, missing_dir, outer_missing_dir].each do |path|
FileUtils.mkdir_p(path)
end
File.write(
File.join(source_dir, 'nextafter.c'),
"#include \"../../../missing/nextafter.c\"\n",
)
File.write(
File.join(missing_dir, 'nextafter.c'),
"#include \"right.h\"\n",
)
File.write(File.join(missing_dir, 'right.h'), '')
File.write(
File.join(outer_missing_dir, 'nextafter.c'),
"#include \"wrong.h\"\n",
)
File.write(File.join(outer_missing_dir, 'wrong.h'), '')
scanner = Scanner.new(
root: root,
include_dirs: [root],
)
assert_equal(
%w[ext/-test-/float/nextafter.c missing/nextafter.c missing/right.h],
scanner.scan('ext/-test-/float/nextafter.c'),
)
end
end
def test_scanners_share_source_file_cache
Dir.mktmpdir('mkdepend-scanner') do |dir|
%w[first.c second.c].each do |source|
File.write(File.join(dir, source), "#include \"common.h\"\n")
end
header = File.join(dir, 'common.h')
File.write(header, "#include \"nested.h\"\n")
File.write(File.join(dir, 'nested.h'), "")
cache = {}
opened = Hash.new(0)
original_open = File.method(:open)
File.define_singleton_method(:open) do |path, *args, **kwargs, &block|
opened[File.expand_path(path)] += 1
original_open.call(path, *args, **kwargs, &block)
end
%w[first.c second.c].each do |source|
scanner = Scanner.new(root: dir, include_dirs: [dir], cache: cache)
assert_include(scanner.scan(source), 'common.h')
end
assert_equal(1, opened[header])
ensure
File.define_singleton_method(:open, original_open) if original_open
end
end
def test_scanner_joins_continued_directives
Dir.mktmpdir('mkdepend-scanner') do |dir|
File.write(File.join(dir, 'main.c'), <<~'C')
#include \
"continued.h"
C
File.write(File.join(dir, 'continued.h'), "")
scanner = Scanner.new(root: dir, include_dirs: [dir])
assert_include(scanner.scan('main.c'), 'continued.h')
end
end
def test_scanner_ignores_template_include
Dir.mktmpdir('mkdepend-scanner') do |dir|
File.write(File.join(dir, 'main.c'), "#include <<%= header %>>\n")
scanner = Scanner.new(root: dir, include_dirs: [dir])
assert_equal(%w[main.c], scanner.scan('main.c'))
assert_empty(scanner.unresolved)
end
end
def test_project_internal_headers_precede_public_headers
Dir.mktmpdir('mkdepend-scanner') do |root|
%w[ext/example internal include/ruby/internal].each do |dir|
FileUtils.mkdir_p(File.join(root, dir))
end
File.write(
File.join(root, 'ext/example/example.c'),
"#include \"internal/gc.h\"\n",
)
File.write(
File.join(root, 'internal/gc.h'),
"#include \"project.h\"\n",
)
File.write(File.join(root, 'internal/project.h'), '')
File.write(
File.join(root, 'include/ruby/internal/gc.h'),
"#include \"public.h\"\n",
)
File.write(File.join(root, 'include/ruby/internal/public.h'), '')
File.write(File.join(root, 'depend'), "#{MARK_START}\n#{MARK_END}\n")
File.write(
File.join(root, 'ext/example/depend'),
"#{MARK_START}\nexample.o: example.c\n#{MARK_END}\n",
)
rules = TestDepend.new(root: root).makedepend('ext/example/example.c').join
assert_include(rules, 'example.o: $(top_srcdir)/internal/gc.h')
assert_include(rules, 'example.o: $(top_srcdir)/internal/project.h')
assert_not_include(rules, 'public.h')
end
end
def test_virtual_dependencies_are_declared
declarations = mkdepend.dependency_declarations('depend')
assert_equal(
%w[include/ruby.h include/ruby/version.h config.h],
declarations.dependencies['config.h'],
)
assert_equal(
declarations.dependencies['config.h'],
declarations.dependencies['ruby/config.h'],
)
assert_equal(%w[probes.h probes.dmyh], declarations.dependencies['probes.h'])
assert_equal(
'prism/templates/src/node.c.erb',
declarations.scan['prism/node.c'],
)
assert_equal(
'prism/templates/include/prism/ast.h.erb',
declarations.scan['prism/ast.h'],
)
assert_equal(
'prism/templates/include/prism/internal/diagnostic.h.erb',
declarations.scan['prism/internal/diagnostic.h'],
)
assert_equal('thread_pthread.h', declarations.scan['THREAD_IMPL_H'])
assert_equal('thread_pthread.c', declarations.scan['THREAD_IMPL_SRC'])
assert_equal(
%w[thread_$(THREAD_MODEL).h],
declarations.dependencies['THREAD_IMPL_H'],
)
assert_equal(
%w[thread_$(THREAD_MODEL).c],
declarations.dependencies['THREAD_IMPL_SRC'],
)
assert_equal(
%w[{$(VPATH)}$(COROUTINE_H)],
declarations.dependencies['COROUTINE_H'],
)
assert_equal(%w[RIPPER], declarations.undefines['parse.y'])
ripper = mkdepend.dependency_declarations('ext/ripper/depend')
assert_equal(%w[RIPPER], ripper.defines['ripper.y'])
assert_equal(
%w[eventids1.h {$(VPATH)}eventids1.c],
ripper.dependencies['eventids1.c'],
)
assert_include(
mkdepend.dependency_targets('ext/socket/depend'),
'constdefs.h',
)
assert_include(
mkdepend.dependency_targets('ext/ripper/depend'),
'eventids1.h',
)
assert_path_not_exist(File.expand_path('../mkdepend', __dir__))
end
def test_scan_patterns_are_expanded_from_selected_root
Dir.mktmpdir('mkdepend-prism') do |dir|
FileUtils.mkdir_p(File.join(dir, 'prism/templates/src'))
FileUtils.mkdir_p(File.join(dir, 'prism/templates/ext/prism'))
FileUtils.mkdir_p(File.join(dir, 'prism/templates/include/prism/internal'))
FileUtils.mkdir_p(File.join(dir, 'enc/trans'))
File.write(File.join(dir, 'prism/templates/src/node.c.erb'), '')
File.write(File.join(dir, 'prism/templates/ext/prism/api.c.erb'), '')
File.write(File.join(dir, 'prism/templates/include/prism/ast.h.erb'), '')
File.write(
File.join(dir, 'prism/templates/include/prism/internal/node.h.erb'),
'',
)
File.write(File.join(dir, 'enc/trans/example.trans'), '')
File.write(File.join(dir, 'depend'), <<~DEPEND)
# mkdepend: scan prism/*.c => prism/templates/src/*.c.erb
# mkdepend: scan prism/*.c => prism/templates/ext/prism/*.c.erb
# mkdepend: scan **/*.h => prism/templates/include/**/*.h.erb
# mkdepend: scan enc/trans/*.c => enc/trans/*.trans
DEPEND
mkdepend = TestDepend.new(root: dir)
assert_equal(
{
'enc/trans/example.c' => 'enc/trans/example.trans',
'prism/api.c' => 'prism/templates/ext/prism/api.c.erb',
'prism/ast.h' => 'prism/templates/include/prism/ast.h.erb',
'prism/internal/node.h' =>
'prism/templates/include/prism/internal/node.h.erb',
'prism/node.c' => 'prism/templates/src/node.c.erb',
},
mkdepend.dependency_declarations('depend').scan,
)
end
end
def test_dependency_declarations_control_scanning_and_output
Dir.mktmpdir('mkdepend-declarations') do |dir|
FileUtils.mkdir_p(File.join(dir, 'ext/example'))
File.write(File.join(dir, 'depend'), <<~DEPEND)
# mkdepend: scan aliased.h => alias.h.tmpl
# mkdepend: depends virtual.h => first.h second.h
#{MARK_START}
#{MARK_END}
DEPEND
File.write(File.join(dir, 'alias.h.tmpl'), "#include \"nested.h\"\n")
File.write(File.join(dir, 'nested.h'), '')
File.write(File.join(dir, 'template.c'), <<~C)
#include "aliased.h"
#include "generated.h"
#include "virtual.h"
#include "vpath.inc"
#include "selected.inc"
C
input = File.join(dir, 'ext/example/depend')
File.write(input, <<~DEPEND)
# mkdepend: scan generated.c => template.c
# mkdepend: depends vpath.inc => {$(VPATH)}vpath.inc
# mkdepend: depends selected.inc => $(SELECTED_HEADER)
generated.h: template.c
#{MARK_START}
generated.o: generated.c
#{MARK_END}
DEPEND
output = File.join(dir, '.deps')
mkdepend = TestDepend.new(root: dir)
assert_true(
mkdepend.run([input], mode: :output, output: output, nmake: true),
)
generated = File.read(File.join(output, 'ext/example/depend'))
assert_include(generated, 'generated.o: generated.c')
assert_include(generated, 'generated.o: generated.h')
assert_include(generated, 'generated.o: {$(VPATH)}vpath.inc')
assert_include(generated, 'generated.o: $(SELECTED_HEADER)')
assert_include(generated, 'generated.o: $(top_srcdir)/first.h')
assert_include(generated, 'generated.o: $(top_srcdir)/second.h')
assert_include(generated, 'generated.o: $(top_srcdir)/nested.h')
end
end
def test_invalid_dependency_declaration
Dir.mktmpdir('mkdepend-declarations') do |dir|
path = File.join(dir, 'depend')
[
'# mkdepend: depends ignored.h =>',
'# mkdepend: generated generated.h',
'# mkdepend: generated generated.h => $(GENERATED_HEADER)',
'# mkdepend: unknown header.h',
'# mkdepend: scan generated/*.c => template.c.erb',
'# mkdepend: scan generated/*.c => missing/*.c.erb',
].each do |declaration|
File.write(path, "manual: rule\n#{declaration}\n")
error = assert_raise(RuntimeError) do
TestDepend.new(root: dir).parse_dependency_declarations(path)
end
assert_equal(
"#{path}:2: invalid mkdepend declaration: #{declaration}",
error.message,
)
end
end
end
def test_duplicate_dependency_declaration
Dir.mktmpdir('mkdepend-declarations') do |dir|
path = File.join(dir, 'depend')
File.write(path, <<~DEPEND)
# mkdepend: depends generated.c => generated.h
# mkdepend: depends generated.c => generated.inc
DEPEND
error = assert_raise(RuntimeError) do
TestDepend.new(root: dir).parse_dependency_declarations(path)
end
assert_equal(
"#{path}:2: duplicate mkdepend declaration: generated.c",
error.message,
)
end
end
def test_dependency_metadata_uses_passed_content
Dir.mktmpdir('mkdepend-declarations') do |dir|
path = File.join(dir, 'missing-depend')
content = <<~DEPEND
# mkdepend: depends generated.h => $(GENERATED_HEADER)
generated.h: template.h
#{MARK_START}
generated.o: generated.c
#{MARK_END}
DEPEND
declarations = TestDepend.new(root: dir).
parse_dependency_declarations(path, content)
assert_equal(%w[$(GENERATED_HEADER)], declarations.dependencies['generated.h'])
assert_equal(
%w[generated.h],
TestDepend.new(root: dir).dependency_targets(path, content),
)
end
end
def test_dependency_files
files = mkdepend.dependency_files
assert_include(files, 'depend')
assert_include(files, 'enc/depend')
assert_include(files, 'ext/date/depend')
assert(files.all? {|file| File.read(File.join(TOP_SRCDIR, file)).include?(MARK_START)})
end
def test_dependency_files_caches_contents
Dir.mktmpdir('mkdepend-files') do |dir|
path = File.join(dir, 'depend')
ignored = File.join(dir, 'ext/example/depend')
FileUtils.mkdir_p(File.dirname(ignored))
content = "#{MARK_START}\n#{MARK_END}\n"
File.write(path, content)
File.write(ignored, "manual: rule\n")
mkdepend = TestDepend.new(root: dir)
assert_equal(%w[depend], mkdepend.dependency_files)
File.unlink(path)
File.unlink(ignored)
assert_equal(content, mkdepend.dependency_file_content(path))
assert_nil(mkdepend.dependency_file_content(ignored))
end
end
def test_dependency_files_can_select_core_files
Dir.mktmpdir('mkdepend-files') do |dir|
%w[depend enc/depend ext/example/depend].each do |file|
path = File.join(dir, file)
FileUtils.mkdir_p(File.dirname(path))
File.write(path, "#{MARK_START}\n#{MARK_END}\n")
end
mkdepend = TestDepend.new(root: dir)
assert_equal(%w[depend enc/depend], mkdepend.dependency_files(:core))
assert_equal(
%w[depend enc/depend ext/example/depend],
mkdepend.dependency_files(:all),
)
assert_equal(
%w[ext/example/depend],
mkdepend.dependency_files(:extensions),
)
end
end
def test_run_selects_extension_files
Dir.mktmpdir('mkdepend-files') do |root|
%w[depend ext/example/depend].each do |file|
path = File.join(root, file)
FileUtils.mkdir_p(File.dirname(path))
File.write(path, "#{MARK_START}\n#{MARK_END}\n")
end
Dir.mktmpdir('mkdepend-output') do |output|
assert_true(
MakeMakefile::Depend.main([
"--root=#{root}", "--scope=extensions", "--output=#{output}",
]),
)
assert_false(File.exist?(File.join(output, 'depend')))
assert_true(File.exist?(File.join(output, 'ext/example/depend')))
end
end
end
def test_thread_model_is_resolved_for_extension_dependencies
Dir.mktmpdir('mkdepend-thread-model') do |root|
FileUtils.mkdir_p(File.join(root, 'ext/example'))
File.write(File.join(root, 'depend'), <<~DEPEND)
# mkdepend: scan THREAD_IMPL_H => thread_$(THREAD_MODEL).h
# mkdepend: depends THREAD_IMPL_H => thread_$(THREAD_MODEL).h
#{MARK_START}
#{MARK_END}
DEPEND
File.write(File.join(root, 'thread_pthread.h'), '')
File.write(
File.join(root, 'thread_win32.h'),
"#include \"win32_only.h\"\n",
)
File.write(File.join(root, 'win32_only.h'), '')
File.write(
File.join(root, 'ext/example/example.c'),
"#include THREAD_IMPL_H\n",
)
File.write(File.join(root, 'ext/example/depend'), <<~DEPEND)
#{MARK_START}
example.o: example.c
#{MARK_END}
DEPEND
mkdepend = TestDepend.new(root: root)
[
{make_variables: {"THREAD_MODEL" => "win32"}},
{thread_model: "win32"},
].each do |options|
out = StringIO.new
mkdepend.run(
[File.join(root, 'ext/example/example.c')],
out: out,
**options,
)
rules = out.string
assert_include(rules, 'example.o: $(top_srcdir)/thread_win32.h')
assert_include(rules, 'example.o: $(top_srcdir)/win32_only.h')
assert_not_include(rules, 'THREAD_MODEL')
assert_not_include(rules, 'thread_pthread.h')
end
out = StringIO.new
mkdepend.run([File.join(root, 'ext/example/example.c')], out: out)
assert_include(
out.string,
'example.o: $(top_srcdir)/thread_$(THREAD_MODEL).h',
)
assert_not_include(out.string, 'thread_win32.h')
end
end
def test_unicode_header
assert_equal(
['$(UNICODE_HDR_DIR)/name2ctype.h'],
mkdepend.depends(['enc/unicode/15.0.0/name2ctype.h'], nil),
)
end
def test_vpath_for_dependency_file
assert_equal('{$(VPATH)}', mkdepend.dependency_vpath('depend', 'enc/ascii.c'))
assert_nil(mkdepend.dependency_vpath('enc/depend', 'enc/ascii.c'))
assert_equal('{$(VPATH)}', mkdepend.dependency_vpath('depend', 'array.c'))
assert_equal(['{$(VPATH)}enc/ascii.c'], mkdepend.depends(['enc/ascii.c'], '{$(VPATH)}'))
assert_equal(['enc/ascii.c'], mkdepend.depends(['enc/ascii.c'], nil))
assert_equal(
['{$(VPATH)}thread_pthread.h'],
mkdepend.depends(['thread_pthread.h'], '{$(VPATH)}'),
)
end
def test_ruby_sources_are_not_headers
assert_empty(mkdepend.depends(%w[array.rb array.rbinc], '{$(VPATH)}') & ['{$(VPATH)}array.rb'])
end
def test_tool_sources
assert_equal(
%w[
$(top_srcdir)/tool/dump_ast.c
$(top_srcdir)/tool/mkdepend/yaml.h
],
mkdepend.depends(%w[tool/dump_ast.c tool/mkdepend/yaml.h], '{$(VPATH)}'),
)
end
def test_dependency_sources
assert_equal(
%w[array.c parse.y],
mkdepend.dependency_sources(<<~RULES, 'depend'),
array.$(OBJEXT): {$(VPATH)}array.c
parse.$(OBJEXT): {$(VPATH)}parse.c
parse.$(OBJEXT): {$(VPATH)}parse.y
RULES
)
assert_equal(
%w[enc/ascii.c],
mkdepend.dependency_sources(<<~RULES, 'enc/depend'),
enc/ascii.$(OBJEXT): enc/ascii.c
RULES
)
assert_equal(
%w[ext/date/date_core.c],
mkdepend.dependency_sources(<<~RULES, 'ext/date/depend'),
date_core.o: date_core.c
RULES
)
assert_equal(
%w[ext/-test-/float/nextafter.c],
mkdepend.dependency_sources(<<~RULES, 'ext/-test-/float/depend'),
nextafter.o: $(top_srcdir)/missing/nextafter.c
nextafter.o: nextafter.c
RULES
)
assert_equal(
%w[ext/-test-/load/dot.dot/dot.dot.c],
mkdepend.dependency_sources(<<~RULES, 'ext/-test-/load/dot.dot/depend'),
dot.dot.o: dot.dot.c
RULES
)
assert_equal(
%w[prism/api_node.c],
mkdepend.dependency_sources(<<~RULES, 'depend'),
prism/api_node.$(OBJEXT): $(top_srcdir)/prism/api_node.c
RULES
)
assert_equal(
{'dump_ast-dump_ast' => 'tool/dump_ast.c'},
mkdepend.dependency_source_map(<<~RULES, 'depend'),
dump_ast-dump_ast.$(OBJEXT): $(top_srcdir)/tool/dump_ast.c
RULES
)
end
def test_extension_headers
assert_equal(
[
'$(RUBY_EXTCONF_H)',
'$(arch_hdrdir)/ruby/config.h',
'$(hdrdir)/ruby.h',
'$(hdrdir)/ruby/internal/intern/parse.h',
'$(hdrdir)/ruby/internal/value.h',
'$(hdrdir)/ruby/version.h',
'$(top_srcdir)/internal.h',
'$(top_srcdir)/internal/parse.h',
'date_tmx.h',
'{$(VPATH)}probes.dmyh',
],
mkdepend.depends(
%w[
extconf.h
config.h
include/ruby/internal/intern/parse.h
include/ruby/internal/value.h
internal.h
internal/parse.h
probes.h
ext/date/date_tmx.h
],
nil,
source: 'ext/date/date_core.c',
),
)
assert_equal(
['$(srcdir)/../digest.h'],
mkdepend.depends(['ext/digest/md5/../digest.h'], nil, source: 'ext/digest/md5/md5.c'),
)
assert_equal(
['{$(VPATH)}ripper.c'],
mkdepend.depends(['ext/ripper/ripper.c'], nil, source: 'ext/ripper/ripper.y'),
)
assert_equal(
['$(top_srcdir)/thread_$(THREAD_MODEL).h'],
mkdepend.depends(['THREAD_IMPL_H'], nil, source: 'ext/coverage/coverage.c'),
)
assert_equal(
['{$(VPATH)}probes.dmyh'],
mkdepend.depends(%w[probes.h probes.dmyh], nil, source: 'ext/ripper/ripper.y'),
)
end
def test_object_name
assert_equal('array.$(OBJEXT)', mkdepend.object_name('array.c'))
assert_equal('enc/utf_8.$(OBJEXT)', mkdepend.object_name('enc/utf_8.c'))
assert_equal('date_core.o', mkdepend.object_name('ext/date/date_core.c'))
assert_equal('ripper.o', mkdepend.object_name('ext/ripper/ripper.y'))
assert_equal('ffs.$(OBJEXT)', mkdepend.object_name('missing/ffs.c'))
end
def test_parse_and_ripper_conditions
assert_not_include(mkdepend.makedepend('parse.y').join, 'eventids1.h')
assert_include(mkdepend.makedepend('ext/ripper/ripper.y').join, 'eventids1.h')
end
def test_generated_headers_are_dependencies
unicode = mkdepend.makedepend('enc/unicode.c').join
assert_include(unicode, '$(UNICODE_HDR_DIR)/casefold.h')
assert_include(unicode, '$(UNICODE_HDR_DIR)/name2ctype.h')
assert_include(mkdepend.makedepend('loadpath.c').join, '{$(VPATH)}verconf.h')
assert_include(mkdepend.makedepend('ruby-runner.c').join, '{$(VPATH)}ruby-runner.h')
end
def test_compact_dependencies
rules = <<~RULES
array.$(OBJEXT): common.h
hash.$(OBJEXT): common.h
array.$(OBJEXT): array.c
hash.$(OBJEXT): hash.c
RULES
assert_equal(<<~DEPEND, mkdepend.compact_dependencies(rules))
array.$(OBJEXT) hash.$(OBJEXT): common.h
array.$(OBJEXT): array.c
hash.$(OBJEXT): hash.c
DEPEND
end
def test_compact_dependencies_keeps_separate_targets_for_nmake
rules = <<~RULES
array.o: common.h
hash.o: common.h
array.o: array.c
hash.o: hash.c
RULES
assert_equal(<<~DEPEND, mkdepend.compact_dependencies(rules, group: false))
array.o: array.c
array.o: common.h
hash.o: common.h
hash.o: hash.c
DEPEND
end
def test_minimize_deps_keeps_only_preferred_source_rules
rules = <<~RULES
array.$(OBJEXT): common.h
array.$(OBJEXT): {$(VPATH)}array.c
parse.$(OBJEXT): {$(VPATH)}parse.c
parse.$(OBJEXT): {$(VPATH)}parse.y
hash.$(OBJEXT): common.h
hash.$(OBJEXT): hash.c
RULES
assert_equal(<<~DEPEND, mkdepend.minimize_deps(rules, 'depend'))
array.$(OBJEXT): {$(VPATH)}array.c
hash.$(OBJEXT): hash.c
parse.$(OBJEXT): {$(VPATH)}parse.y
DEPEND
end
def test_minimize_deps_keeps_upstream_dependencies
rules = <<~RULES
generator.o: $(hdrdir)/ruby.h
generator.o: $(srcdir)/../json.h
generator.o: generator.c
RULES
expected = <<~DEPEND
generator.o: $(srcdir)/../json.h
generator.o: generator.c
DEPEND
assert_equal(
expected,
mkdepend.minimize_deps(rules, 'ext/json/generator/depend'),
)
end
def test_sources_regenerates_upstream_dependencies
Dir.mktmpdir('mkdepend') do |root|
source_dir = File.join(root, 'ext/example')
FileUtils.mkdir_p(source_dir)
File.write(File.join(root, 'ext/current.h'), '')
File.write(File.join(source_dir, 'example.c'), <<~SOURCE)
#include "../current.h"
SOURCE
depend = File.join(source_dir, 'depend')
File.write(depend, <<~DEPEND)
#{MARK_START}
example.o: $(srcdir)/../stale.h
example.o: example.c
#{MARK_END}
DEPEND
TestDepend.new(root: root).run(
[depend], sources: true, mode: :inplace,
)
assert_equal(<<~DEPEND, File.read(depend))
#{MARK_START}
example.o: $(srcdir)/../current.h
example.o: example.c
#{MARK_END}
DEPEND
end
end
def test_minimize_deps_preserves_text_outside_markers
content = +<<~DEPEND
manual: rule
#{MARK_START}
array.$(OBJEXT): common.h
array.$(OBJEXT): {$(VPATH)}array.c
#{MARK_END}
upstream: rule
DEPEND
Dir.mktmpdir('mkdepend') do |dir|
path = File.join(dir, 'depend')
File.write(path, content)
assert_false(
mkdepend.run(
[path], sources: true, mode: :check, err: StringIO.new,
),
)
assert_true(mkdepend.run([path], sources: true, mode: :inplace))
assert_equal(<<~DEPEND, File.read(path))
manual: rule
#{MARK_START}
array.$(OBJEXT): {$(VPATH)}array.c
#{MARK_END}
upstream: rule
DEPEND
end
end
def test_update_extension_uses_explicit_source_map
Dir.mktmpdir('mkdepend-extension') do |root|
source_dir = File.join(root, 'ext/example')
FileUtils.mkdir_p(source_dir)
FileUtils.mkdir_p(File.join(root, 'include'))
File.write(File.join(root, 'ext/shared.h'), '')
File.write(File.join(root, 'include/project.h'), '')
File.write(File.join(source_dir, 'example.c'), <<~SOURCE)
#include "../shared.h"
#include "../../include/project.h"
SOURCE
depend = File.join(source_dir, 'depend')
File.write(depend, <<~DEPEND)
#{MARK_START}
stale.o: stale.c
#{MARK_END}
DEPEND
MakeMakefile::Depend.new(root: root).update_extension(
depend,
{'example' => File.join(source_dir, 'example.c')},
)
assert_equal(<<~DEPEND, File.read(depend))
#{MARK_START}
example.o: $(srcdir)/../../include/project.h
example.o: $(srcdir)/../shared.h
example.o: example.c
#{MARK_END}
DEPEND
end
end
def test_update_extension_expands_make_variables_in_manual_rules
Dir.mktmpdir('mkdepend-extension') do |root|
source_dir = File.join(root, 'ext/example')
FileUtils.mkdir_p(source_dir)
File.write(File.join(source_dir, 'local.h'), '')
File.write(
File.join(source_dir, 'example.c'),
"#include \"local.h\"\n",
)
depend = File.join(source_dir, 'depend')
File.write(depend, <<~DEPEND)
example.o: $(LOCAL_HDRS)
#{MARK_START}
stale.o: stale.c
#{MARK_END}
DEPEND
MakeMakefile::Depend.new(root: root).update_extension(
depend,
{'example' => File.join(source_dir, 'example.c')},
make_variables: {"LOCAL_HDRS" => ["{.;$(VPATH)}local.h"]},
)
assert_equal(<<~DEPEND, File.read(depend))
example.o: $(LOCAL_HDRS)
#{MARK_START}
example.o: example.c
example.o: local.h
#{MARK_END}
DEPEND
end
end
def test_update_extension_requires_markers
Dir.mktmpdir('mkdepend-extension') do |root|
source_dir = File.join(root, 'ext/example')
FileUtils.mkdir_p(source_dir)
File.write(File.join(source_dir, 'example.c'), '')
depend = File.join(source_dir, 'depend')
File.write(depend, "example.o: example.c\n")
error = assert_raise(RuntimeError) do
MakeMakefile::Depend.new(root: root).update_extension(
depend,
{'example' => File.join(source_dir, 'example.c')},
)
end
assert_include(error.message, "no autogenerated dependency section")
end
end
def test_relative_source
assert_equal('array.c', mkdepend.relative_source(File.join(TOP_SRCDIR, 'array.c')))
assert_equal(
'array.c',
mkdepend.relative_source(File.join(TOP_SRCDIR, '.+aarch64-darwin', '..', 'array.c')),
)
end
def test_replace_file_without_leftovers
Dir.mktmpdir('mkdepend') do |dir|
path = File.join(dir, 'depend')
File.write(path, <<~DEPEND)
#{MARK_START}
array.$(OBJEXT): {$(VPATH)}array.c
#{MARK_END}
DEPEND
File.chmod(0644, path)
content = File.read(path)
assert_true(mkdepend.run([path], mode: :inplace))
assert_include(File.read(path), 'array.$(OBJEXT): $(hdrdir)/ruby/ruby.h')
assert_equal(0644, File.stat(path).mode & 0777)
assert_equal(%w[depend], Dir.children(dir))
end
end
def test_run_writes_expanded_dependency_to_output_directory
Dir.mktmpdir('mkdepend-output') do |dir|
source = File.join(TOP_SRCDIR, 'ext/date/depend')
output = File.join(dir, 'build-deps')
original = File.read(source)
assert_true(mkdepend.run([source], mode: :output, output: output))
generated = File.read(File.join(output, 'ext/date/depend'))
assert_include(generated, 'date_core.o: date_core.c')
assert_match(/date_core\.o.*: \$\(hdrdir\)\/ruby\/ruby\.h/, generated)
assert_not_include(generated, '{$(VPATH)}')
assert_equal(original, File.read(source))
end
end
def test_run_reuses_unchanged_output_dependencies
Dir.mktmpdir('mkdepend-input') do |root|
File.write(File.join(root, 'one.c'), "#include \"common.h\"\n")
header = File.join(root, 'common.h')
File.write(header, "")
input = File.join(root, 'depend')
File.write(input, <<~DEPEND)
#{MARK_START}
one.$(OBJEXT): one.c
#{MARK_END}
DEPEND
output = File.join(root, '.deps')
destination = File.join(output, 'depend')
updater = TestDepend.new(root: root)
assert_true(updater.run([input], mode: :output, output: output))
assert_path_exist(destination + '.mkdepend')
opened = []
original_open = File.method(:open)
File.define_singleton_method(:open) do |path, *args, **kwargs, &block|
opened << File.expand_path(path)
original_open.call(path, *args, **kwargs, &block)
end
updater = TestDepend.new(root: root)
assert_true(updater.run([input], mode: :output, output: output))
assert_empty(opened & [File.join(root, 'one.c'), header])
future = Time.now + 2
File.utime(future, future, header)
updater = TestDepend.new(root: root)
assert_true(updater.run([input], mode: :output, output: output))
assert_include(opened, header)
ensure
File.define_singleton_method(:open, original_open) if original_open
end
end
def test_run_invalidates_output_when_missing_header_appears
Dir.mktmpdir('mkdepend-input') do |root|
File.write(File.join(root, 'one.c'), "#include \"appeared.h\"\n")
input = File.join(root, 'depend')
File.write(input, <<~DEPEND)
#{MARK_START}
one.$(OBJEXT): one.c
#{MARK_END}
DEPEND
output = File.join(root, '.deps')
destination = File.join(output, 'depend')
updater = TestDepend.new(root: root)
assert_true(updater.run([input], mode: :output, output: output))
assert_not_include(File.read(destination), 'appeared.h')
File.write(File.join(root, 'appeared.h'), "")
assert_true(updater.run([input], mode: :output, output: output))
assert_include(File.read(destination), 'one.$(OBJEXT): appeared.h')
end
end
def test_run_reuses_output_with_non_directory_include_prefix
Dir.mktmpdir('mkdepend-input') do |root|
File.write(File.join(root, 'ruby'), "")
File.write(File.join(root, 'one.c'), "#include \"ruby/assert.h\"\n")
input = File.join(root, 'depend')
File.write(input, <<~DEPEND)
#{MARK_START}
one.$(OBJEXT): one.c
#{MARK_END}
DEPEND
output = File.join(root, '.deps')
updater = TestDepend.new(root: root)
assert_true(updater.run([input], mode: :output, output: output))
updater = TestDepend.new(root: root)
assert_true(updater.run([input], mode: :output, output: output))
end
end
def test_run_preserves_vpath_notation_for_nmake_output
Dir.mktmpdir('mkdepend-output') do |dir|
source = File.join(TOP_SRCDIR, 'ext/-test-/thread/id/depend')
assert_true(
mkdepend.run([source], mode: :output, output: dir, nmake: true),
)
generated = File.read(File.join(dir, 'ext/-test-/thread/id/depend'))
assert_include(generated, '{$(VPATH)}id.c')
end
end
def test_run_removes_vpath_notation_from_build_output
Dir.mktmpdir('mkdepend-output') do |dir|
source = File.join(TOP_SRCDIR, 'ext/-test-/thread/id/depend')
assert_true(mkdepend.run([source], mode: :output, output: dir))
generated = File.read(File.join(dir, 'ext/-test-/thread/id/depend'))
assert_include(generated, 'id.o: id.c')
assert_not_include(generated, '{$(VPATH)}')
end
end
def test_run_from_build_directory_keeps_generated_dependency_names
Dir.mktmpdir('mkdepend-builddir') do |dir|
File.write(File.join(dir, 'builtin.c'), <<~SOURCE)
#include "builtin_binary.rbbin"
SOURCE
input = File.join(dir, 'depend')
File.write(input, <<~DEPEND)
#{MARK_START}
builtin.$(OBJEXT): {$(VPATH)}builtin.c
#{MARK_END}
DEPEND
build = File.join(dir, '.build')
FileUtils.mkdir_p(build)
File.write(File.join(build, 'builtin_binary.rbbin'), '')
output = File.join(build, '.deps')
mkdepend = TestDepend.new(root: dir)
Dir.chdir(build) do
assert_true(mkdepend.run([input], mode: :output, output: output))
end
generated = File.read(File.join(output, 'depend'))
assert_include(generated, "builtin.$(OBJEXT): builtin_binary.rbbin\n")
assert_not_include(generated, '.build/builtin_binary.rbbin')
end
end
def test_build_directory_does_not_shadow_source_tree_names
Dir.mktmpdir('mkdepend-builddir') do |root|
FileUtils.mkdir_p(File.join(root, 'ext/example'))
File.write(File.join(root, 'ext/example/depend'), <<~DEPEND)
# mkdepend: depends example.c => {$(VPATH)}example.c
DEPEND
build = File.join(root, 'build')
FileUtils.mkdir_p(File.join(build, 'ext/example'))
File.write(File.join(build, 'ext/example/example.c'), '')
mkdepend = TestDepend.new(root: root)
Dir.chdir(build) do
assert_equal(
'ext/example/depend',
mkdepend.dependency_input('ext/example/example.c'),
)
assert_equal(
['{$(VPATH)}example.c'],
mkdepend.depends(%w[ext/example/example.c], nil,
source: 'ext/example/example.c'),
)
end
end
end
def test_normalize_dependency_rules_removes_vpath_search
assert_equal(
"one.h two.h\n",
mkdepend.normalize_dependency_rules(
"{$(VPATH)}one.h {.;$(VPATH)}two.h\n",
),
)
end
def test_run_writes_unchanged_dependencies_to_output_directory
Dir.mktmpdir('mkdepend-input') do |input_dir|
Dir.mktmpdir('mkdepend-output') do |output|
mkdepend = TestDepend.new(root: input_dir)
input = File.join(input_dir, 'depend')
File.write(File.join(input_dir, 'array.c'), '')
rules = "array.$(OBJEXT): {$(VPATH)}array.c\n"
File.write(input, "#{MARK_START}\n#{rules}#{MARK_END}\n")
destination = File.join(output, 'depend')
assert_true(mkdepend.run([input], mode: :output, output: output))
assert_equal(
mkdepend.normalize_dependency_rules(File.read(input)),
File.read(destination),
)
end
end
end
def test_file_without_markers_is_not_updated
Dir.mktmpdir('mkdepend') do |dir|
path = File.join(dir, 'depend')
content = "array.$(OBJEXT): {$(VPATH)}array.c\n"
File.write(path, content)
assert_true(mkdepend.run([path], mode: :inplace))
assert_equal(content, File.read(path))
end
end
def test_unterminated_dependency_section_is_rejected
content = +<<~DEPEND
#{MARK_START}
array.$(OBJEXT): {$(VPATH)}array.c
DEPEND
[content, MARK_START].each do |deps|
Dir.mktmpdir('mkdepend') do |dir|
path = File.join(dir, 'depend')
File.write(path, deps)
error = assert_raise(RuntimeError) {mkdepend.run([path])}
assert_include(error.message, "missing #{MARK_END}")
end
end
end
def test_object_rules_without_a_source_are_rejected
content = +<<~DEPEND
#{MARK_START}
unknown.o: unknown.h
#{MARK_END}
DEPEND
Dir.mktmpdir('mkdepend') do |dir|
path = File.join(dir, 'depend')
File.write(path, content)
error = assert_raise(RuntimeError) {mkdepend.run([path])}
assert_include(error.message, 'no source files')
end
end
def test_missing_dependency_source_is_rejected
rules = <<~DEPEND
unknown.o: unknown.c
DEPEND
error = assert_raise(RuntimeError) do
mkdepend.minimize_deps(rules, 'ext/test/depend')
end
assert_include(error.message, 'source file not found: ext/test/unknown.c')
error = assert_raise(RuntimeError) do
mkdepend.update_deps(rules, 'ext/test/depend')
end
assert_include(error.message, 'source file not found: ext/test/unknown.c')
end
def test_vpath_notation_does_not_affect_dependency_comparison
input = 'ext/-test-/thread/id/depend'
expected = mkdepend.compact_dependencies(
mkdepend.makedepend('ext/-test-/thread/id/id.c', input: input).join,
)
plain = expected.gsub('{$(VPATH)}', '')
assert_true(mkdepend.same_dependency_rules?(expected, plain))
assert_true(mkdepend.same_dependency_rules?(plain, expected))
assert_true(mkdepend.same_dependency_rules?(plain, mkdepend.update_deps(plain, input)))
end
def test_outdated_dependency_report_shows_small_rule_changes
current = <<~RULES
array.$(OBJEXT): common.h
array.$(OBJEXT): {$(VPATH)}array.c
RULES
expected = "array.$(OBJEXT): {$(VPATH)}array.c\n"
err = StringIO.new
mkdepend.report_outdated_dependencies('depend', current, expected, err: err)
assert_include(err.string, 'depend: dependencies are outdated')
assert_include(err.string, 'current dependency rules:')
assert_include(err.string, 'array.$(OBJEXT): common.h')
assert_include(err.string, 'expected source rules:')
end
def test_outdated_dependency_report_summarizes_large_rule_changes
headers = 25.times.map {|i| "array.$(OBJEXT): header#{i}.h\n"}.join
current = headers + "array.$(OBJEXT): {$(VPATH)}array.c\n"
expected = "array.$(OBJEXT): {$(VPATH)}array.c\n"
err = StringIO.new
mkdepend.report_outdated_dependencies('depend', current, expected, err: err)
assert_include(err.string, 'current section has 26 rules; expected 1 source rule')
assert_not_include(err.string, 'header0.h')
assert_include(err.string, expected.strip)
end
end