nanoc 4.7.0 → 4.7.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +4 -4
- data/NEWS.md +12 -1
- data/README.md +1 -1
- data/lib/nanoc/base/entities/directed_graph.rb +16 -0
- data/lib/nanoc/base/entities/identifiable_collection.rb +24 -8
- data/lib/nanoc/base/entities/outdatedness_reasons.rb +5 -0
- data/lib/nanoc/base/errors.rb +11 -6
- data/lib/nanoc/base/memoization.rb +6 -23
- data/lib/nanoc/base/services/compiler/phases/abstract.rb +3 -3
- data/lib/nanoc/base/services/compiler/phases/cache.rb +1 -3
- data/lib/nanoc/base/services/compiler/phases/mark_done.rb +1 -3
- data/lib/nanoc/base/services/compiler/phases/recalculate.rb +1 -3
- data/lib/nanoc/base/services/compiler/phases/resume.rb +1 -3
- data/lib/nanoc/base/services/compiler/phases/write.rb +1 -3
- data/lib/nanoc/base/services/filter.rb +15 -0
- data/lib/nanoc/base/services/item_rep_selector.rb +1 -1
- data/lib/nanoc/base/services/outdatedness_checker.rb +3 -1
- data/lib/nanoc/base/services/outdatedness_rule.rb +7 -0
- data/lib/nanoc/base/services/outdatedness_rules.rb +16 -0
- data/lib/nanoc/cli/commands/compile.rb +12 -411
- data/lib/nanoc/cli/commands/compile_listeners/abstract.rb +30 -0
- data/lib/nanoc/cli/commands/compile_listeners/debug_printer.rb +34 -0
- data/lib/nanoc/cli/commands/compile_listeners/diff_generator.rb +91 -0
- data/lib/nanoc/cli/commands/compile_listeners/file_action_printer.rb +61 -0
- data/lib/nanoc/cli/commands/compile_listeners/stack_prof_profiler.rb +22 -0
- data/lib/nanoc/cli/commands/compile_listeners/timing_recorder.rb +174 -0
- data/lib/nanoc/filters/xsl.rb +2 -0
- data/lib/nanoc/version.rb +1 -1
- data/spec/nanoc/base/directed_graph_spec.rb +54 -0
- data/spec/nanoc/base/errors/dependency_cycle_spec.rb +32 -0
- data/spec/nanoc/base/filter_spec.rb +36 -0
- data/spec/nanoc/base/services/compiler/phases/abstract_spec.rb +23 -11
- data/spec/nanoc/base/services/outdatedness_rules_spec.rb +41 -0
- data/spec/nanoc/cli/commands/compile/file_action_printer_spec.rb +1 -1
- data/spec/nanoc/cli/commands/compile/timing_recorder_spec.rb +124 -54
- data/spec/nanoc/cli/commands/compile_spec.rb +1 -1
- data/spec/nanoc/regressions/gh_1040_spec.rb +1 -1
- data/spec/nanoc/regressions/gh_924_spec.rb +89 -0
- data/test/base/test_compiler.rb +1 -1
- data/test/base/test_memoization.rb +6 -0
- data/test/cli/commands/test_compile.rb +2 -2
- metadata +12 -3
@@ -0,0 +1,30 @@
|
|
1
|
+
module Nanoc::CLI::Commands::CompileListeners
|
2
|
+
class Abstract
|
3
|
+
def initialize(*); end
|
4
|
+
|
5
|
+
def self.enable_for?(command_runner) # rubocop:disable Lint/UnusedMethodArgument
|
6
|
+
true
|
7
|
+
end
|
8
|
+
|
9
|
+
def start
|
10
|
+
raise NotImplementedError, "Subclasses of #{self.class} must implement #start"
|
11
|
+
end
|
12
|
+
|
13
|
+
def stop; end
|
14
|
+
|
15
|
+
def start_safely
|
16
|
+
start
|
17
|
+
@_started = true
|
18
|
+
end
|
19
|
+
|
20
|
+
def stop_safely
|
21
|
+
stop if @_started
|
22
|
+
@_started = false
|
23
|
+
end
|
24
|
+
|
25
|
+
def on(sym)
|
26
|
+
# TODO: clean up on stop
|
27
|
+
Nanoc::Int::NotificationCenter.on(sym, self) { |*args| yield(*args) }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Nanoc::CLI::Commands::CompileListeners
|
2
|
+
class DebugPrinter < Abstract
|
3
|
+
# @see Listener#enable_for?
|
4
|
+
def self.enable_for?(command_runner)
|
5
|
+
command_runner.debug?
|
6
|
+
end
|
7
|
+
|
8
|
+
# @see Listener#start
|
9
|
+
def start
|
10
|
+
Nanoc::Int::NotificationCenter.on(:compilation_started) do |rep|
|
11
|
+
puts "*** Started compilation of #{rep.inspect}"
|
12
|
+
end
|
13
|
+
Nanoc::Int::NotificationCenter.on(:compilation_ended) do |rep|
|
14
|
+
puts "*** Ended compilation of #{rep.inspect}"
|
15
|
+
puts
|
16
|
+
end
|
17
|
+
Nanoc::Int::NotificationCenter.on(:compilation_suspended) do |rep, e|
|
18
|
+
puts "*** Suspended compilation of #{rep.inspect}: #{e.message}"
|
19
|
+
end
|
20
|
+
Nanoc::Int::NotificationCenter.on(:cached_content_used) do |rep|
|
21
|
+
puts "*** Used cached compiled content for #{rep.inspect} instead of recompiling"
|
22
|
+
end
|
23
|
+
Nanoc::Int::NotificationCenter.on(:filtering_started) do |rep, filter_name|
|
24
|
+
puts "*** Started filtering #{rep.inspect} with #{filter_name}"
|
25
|
+
end
|
26
|
+
Nanoc::Int::NotificationCenter.on(:filtering_ended) do |rep, filter_name|
|
27
|
+
puts "*** Ended filtering #{rep.inspect} with #{filter_name}"
|
28
|
+
end
|
29
|
+
Nanoc::Int::NotificationCenter.on(:dependency_created) do |src, dst|
|
30
|
+
puts "*** Dependency created from #{src.inspect} onto #{dst.inspect}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module Nanoc::CLI::Commands::CompileListeners
|
2
|
+
class DiffGenerator < Abstract
|
3
|
+
# @see Listener#enable_for?
|
4
|
+
def self.enable_for?(command_runner)
|
5
|
+
command_runner.site.config[:enable_output_diff]
|
6
|
+
end
|
7
|
+
|
8
|
+
# @see Listener#start
|
9
|
+
def start
|
10
|
+
require 'tempfile'
|
11
|
+
setup_diffs
|
12
|
+
old_contents = {}
|
13
|
+
Nanoc::Int::NotificationCenter.on(:will_write_rep, self) do |rep, path|
|
14
|
+
old_contents[rep] = File.file?(path) ? File.read(path) : nil
|
15
|
+
end
|
16
|
+
Nanoc::Int::NotificationCenter.on(:rep_written, self) do |rep, binary, path, _is_created, _is_modified|
|
17
|
+
unless binary
|
18
|
+
new_contents = File.file?(path) ? File.read(path) : nil
|
19
|
+
if old_contents[rep] && new_contents
|
20
|
+
generate_diff_for(path, old_contents[rep], new_contents)
|
21
|
+
end
|
22
|
+
old_contents.delete(rep)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# @see Listener#stop
|
28
|
+
def stop
|
29
|
+
super
|
30
|
+
|
31
|
+
Nanoc::Int::NotificationCenter.remove(:will_write_rep, self)
|
32
|
+
Nanoc::Int::NotificationCenter.remove(:rep_written, self)
|
33
|
+
|
34
|
+
teardown_diffs
|
35
|
+
end
|
36
|
+
|
37
|
+
protected
|
38
|
+
|
39
|
+
def setup_diffs
|
40
|
+
@diff_lock = Mutex.new
|
41
|
+
@diff_threads = []
|
42
|
+
FileUtils.rm('output.diff') if File.file?('output.diff')
|
43
|
+
end
|
44
|
+
|
45
|
+
def teardown_diffs
|
46
|
+
@diff_threads.each(&:join)
|
47
|
+
end
|
48
|
+
|
49
|
+
def generate_diff_for(path, old_content, new_content)
|
50
|
+
return if old_content == new_content
|
51
|
+
|
52
|
+
@diff_threads << Thread.new do
|
53
|
+
# Generate diff
|
54
|
+
diff = diff_strings(old_content, new_content)
|
55
|
+
diff.sub!(/^--- .*/, '--- ' + path)
|
56
|
+
diff.sub!(/^\+\+\+ .*/, '+++ ' + path)
|
57
|
+
|
58
|
+
# Write diff
|
59
|
+
@diff_lock.synchronize do
|
60
|
+
File.open('output.diff', 'a') { |io| io.write(diff) }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def diff_strings(a, b)
|
66
|
+
require 'open3'
|
67
|
+
|
68
|
+
# Create files
|
69
|
+
Tempfile.open('old') do |old_file|
|
70
|
+
Tempfile.open('new') do |new_file|
|
71
|
+
# Write files
|
72
|
+
old_file.write(a)
|
73
|
+
old_file.flush
|
74
|
+
new_file.write(b)
|
75
|
+
new_file.flush
|
76
|
+
|
77
|
+
# Diff
|
78
|
+
cmd = ['diff', '-u', old_file.path, new_file.path]
|
79
|
+
Open3.popen3(*cmd) do |_stdin, stdout, _stderr|
|
80
|
+
result = stdout.read
|
81
|
+
return (result == '' ? nil : result)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
rescue Errno::ENOENT
|
86
|
+
warn 'Failed to run `diff`, so no diff with the previously compiled ' \
|
87
|
+
'content will be available.'
|
88
|
+
nil
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Nanoc::CLI::Commands::CompileListeners
|
2
|
+
class FileActionPrinter < Abstract
|
3
|
+
def initialize(reps:)
|
4
|
+
@start_times = {}
|
5
|
+
@acc_durations = {}
|
6
|
+
|
7
|
+
@reps = reps
|
8
|
+
end
|
9
|
+
|
10
|
+
# @see Listener#start
|
11
|
+
def start
|
12
|
+
Nanoc::Int::NotificationCenter.on(:compilation_started, self) do |rep|
|
13
|
+
@start_times[rep] = Time.now
|
14
|
+
@acc_durations[rep] ||= 0.0
|
15
|
+
end
|
16
|
+
|
17
|
+
Nanoc::Int::NotificationCenter.on(:compilation_suspended, self) do |rep|
|
18
|
+
@acc_durations[rep] += Time.now - @start_times[rep]
|
19
|
+
end
|
20
|
+
|
21
|
+
Nanoc::Int::NotificationCenter.on(:rep_written, self) do |rep, _binary, path, is_created, is_modified|
|
22
|
+
@acc_durations[rep] += Time.now - @start_times[rep]
|
23
|
+
duration = @acc_durations[rep]
|
24
|
+
|
25
|
+
action =
|
26
|
+
if is_created then :create
|
27
|
+
elsif is_modified then :update
|
28
|
+
else :identical
|
29
|
+
end
|
30
|
+
level =
|
31
|
+
if is_created then :high
|
32
|
+
elsif is_modified then :high
|
33
|
+
else :low
|
34
|
+
end
|
35
|
+
log(level, action, path, duration)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# @see Listener#stop
|
40
|
+
def stop
|
41
|
+
super
|
42
|
+
|
43
|
+
Nanoc::Int::NotificationCenter.remove(:compilation_started, self)
|
44
|
+
Nanoc::Int::NotificationCenter.remove(:compilation_suspended, self)
|
45
|
+
Nanoc::Int::NotificationCenter.remove(:rep_written, self)
|
46
|
+
|
47
|
+
@reps.reject(&:compiled?).each do |rep|
|
48
|
+
raw_paths = rep.raw_paths.values.flatten.uniq
|
49
|
+
raw_paths.each do |raw_path|
|
50
|
+
log(:low, :skip, raw_path, nil)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def log(level, action, path, duration)
|
58
|
+
Nanoc::CLI::Logger.instance.file(level, action, path, duration)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Nanoc::CLI::Commands::CompileListeners
|
2
|
+
class StackProfProfiler < Abstract
|
3
|
+
PROFILE_FILE = 'tmp/stackprof_profile'.freeze
|
4
|
+
|
5
|
+
# @see Listener#enable_for?
|
6
|
+
def self.enable_for?(command_runner)
|
7
|
+
command_runner.options.fetch(:profile, false)
|
8
|
+
end
|
9
|
+
|
10
|
+
# @see Listener#start
|
11
|
+
def start
|
12
|
+
require 'stackprof'
|
13
|
+
StackProf.start(mode: :cpu)
|
14
|
+
end
|
15
|
+
|
16
|
+
# @see Listener#stop
|
17
|
+
def stop
|
18
|
+
StackProf.stop
|
19
|
+
StackProf.results(PROFILE_FILE)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
module Nanoc::CLI::Commands::CompileListeners
|
2
|
+
class TimingRecorder < Abstract
|
3
|
+
attr_reader :telemetry
|
4
|
+
|
5
|
+
# @see Listener#enable_for?
|
6
|
+
def self.enable_for?(command_runner)
|
7
|
+
command_runner.options.fetch(:verbose, false)
|
8
|
+
end
|
9
|
+
|
10
|
+
# @param [Enumerable<Nanoc::Int::ItemRep>] reps
|
11
|
+
def initialize(reps:)
|
12
|
+
@reps = reps
|
13
|
+
@telemetry = Nanoc::Telemetry.new
|
14
|
+
end
|
15
|
+
|
16
|
+
# @see Listener#start
|
17
|
+
def start
|
18
|
+
stage_stopwatch = Nanoc::Telemetry::Stopwatch.new
|
19
|
+
|
20
|
+
on(:stage_started) do |_stage_name|
|
21
|
+
stage_stopwatch.start
|
22
|
+
end
|
23
|
+
|
24
|
+
on(:stage_ended) do |stage_name|
|
25
|
+
stage_stopwatch.stop
|
26
|
+
@telemetry.summary(:stages).observe(stage_stopwatch.duration, stage_name.to_s)
|
27
|
+
stage_stopwatch = Nanoc::Telemetry::Stopwatch.new
|
28
|
+
end
|
29
|
+
|
30
|
+
outdatedness_rule_stopwatches = {}
|
31
|
+
|
32
|
+
on(:outdatedness_rule_started) do |klass, obj|
|
33
|
+
stopwatches = outdatedness_rule_stopwatches.fetch(klass) { outdatedness_rule_stopwatches[klass] = {} }
|
34
|
+
stopwatch = stopwatches.fetch(obj) { stopwatches[obj] = Nanoc::Telemetry::Stopwatch.new }
|
35
|
+
stopwatch.start
|
36
|
+
end
|
37
|
+
|
38
|
+
on(:outdatedness_rule_ended) do |klass, obj|
|
39
|
+
stopwatches = outdatedness_rule_stopwatches.fetch(klass)
|
40
|
+
stopwatch = stopwatches.fetch(obj)
|
41
|
+
stopwatch.stop
|
42
|
+
|
43
|
+
@telemetry.summary(:outdatedness_rules).observe(stopwatch.duration, klass.to_s.sub(/.*::/, ''))
|
44
|
+
end
|
45
|
+
|
46
|
+
filter_stopwatches = {}
|
47
|
+
|
48
|
+
on(:filtering_started) do |rep, _filter_name|
|
49
|
+
stopwatch_stack = filter_stopwatches.fetch(rep) { filter_stopwatches[rep] = [] }
|
50
|
+
stopwatch_stack << Nanoc::Telemetry::Stopwatch.new
|
51
|
+
stopwatch_stack.last.start
|
52
|
+
end
|
53
|
+
|
54
|
+
on(:filtering_ended) do |rep, filter_name|
|
55
|
+
stopwatch = filter_stopwatches.fetch(rep).pop
|
56
|
+
stopwatch.stop
|
57
|
+
|
58
|
+
@telemetry.summary(:filters).observe(stopwatch.duration, filter_name.to_s)
|
59
|
+
end
|
60
|
+
|
61
|
+
on(:compilation_suspended) do |rep, _exception|
|
62
|
+
filter_stopwatches.fetch(rep).each(&:stop)
|
63
|
+
end
|
64
|
+
|
65
|
+
on(:compilation_started) do |rep|
|
66
|
+
filter_stopwatches.fetch(rep, []).each(&:start)
|
67
|
+
end
|
68
|
+
|
69
|
+
phase_stopwatches = {}
|
70
|
+
|
71
|
+
on(:phase_started) do |phase_name, rep|
|
72
|
+
stopwatches = phase_stopwatches.fetch(rep) { phase_stopwatches[rep] = {} }
|
73
|
+
stopwatches[phase_name] = Nanoc::Telemetry::Stopwatch.new.tap(&:start)
|
74
|
+
end
|
75
|
+
|
76
|
+
on(:phase_ended) do |phase_name, rep|
|
77
|
+
stopwatch = phase_stopwatches.fetch(rep).fetch(phase_name)
|
78
|
+
stopwatch.stop
|
79
|
+
|
80
|
+
@telemetry.summary(:phases).observe(stopwatch.duration, phase_name)
|
81
|
+
end
|
82
|
+
|
83
|
+
on(:phase_yielded) do |phase_name, rep|
|
84
|
+
stopwatch = phase_stopwatches.fetch(rep).fetch(phase_name)
|
85
|
+
stopwatch.stop
|
86
|
+
end
|
87
|
+
|
88
|
+
on(:phase_resumed) do |phase_name, rep|
|
89
|
+
stopwatch = phase_stopwatches.fetch(rep).fetch(phase_name)
|
90
|
+
stopwatch.start if stopwatch.stopped?
|
91
|
+
end
|
92
|
+
|
93
|
+
on(:phase_aborted) do |phase_name, rep|
|
94
|
+
stopwatch = phase_stopwatches.fetch(rep).fetch(phase_name)
|
95
|
+
stopwatch.stop if stopwatch.running?
|
96
|
+
|
97
|
+
@telemetry.summary(:phases).observe(stopwatch.duration, phase_name)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# @see Listener#stop
|
102
|
+
def stop
|
103
|
+
print_profiling_feedback
|
104
|
+
super
|
105
|
+
end
|
106
|
+
|
107
|
+
protected
|
108
|
+
|
109
|
+
def table_for_summary(name)
|
110
|
+
headers = [name.to_s, 'count', 'min', '.50', '.90', '.95', 'max', 'tot']
|
111
|
+
|
112
|
+
rows = @telemetry.summary(name).map do |filter_name, summary|
|
113
|
+
count = summary.count
|
114
|
+
min = summary.min
|
115
|
+
p50 = summary.quantile(0.50)
|
116
|
+
p90 = summary.quantile(0.90)
|
117
|
+
p95 = summary.quantile(0.95)
|
118
|
+
tot = summary.sum
|
119
|
+
max = summary.max
|
120
|
+
|
121
|
+
[filter_name, count.to_s] + [min, p50, p90, p95, max, tot].map { |r| "#{format('%4.2f', r)}s" }
|
122
|
+
end
|
123
|
+
|
124
|
+
[headers] + rows
|
125
|
+
end
|
126
|
+
|
127
|
+
def table_for_summary_durations(name)
|
128
|
+
headers = [name.to_s, 'tot']
|
129
|
+
|
130
|
+
rows = @telemetry.summary(:stages).map do |stage_name, summary|
|
131
|
+
[stage_name, "#{format('%4.2f', summary.sum)}s"]
|
132
|
+
end
|
133
|
+
|
134
|
+
[headers] + rows
|
135
|
+
end
|
136
|
+
|
137
|
+
def print_profiling_feedback
|
138
|
+
print_table_for_summary(:filters)
|
139
|
+
print_table_for_summary(:phases) if Nanoc::CLI.verbosity >= 2
|
140
|
+
print_table_for_summary_duration(:stages) if Nanoc::CLI.verbosity >= 2
|
141
|
+
print_table_for_summary(:outdatedness_rules) if Nanoc::CLI.verbosity >= 2
|
142
|
+
end
|
143
|
+
|
144
|
+
def print_table_for_summary(name)
|
145
|
+
return if @telemetry.summary(name).empty?
|
146
|
+
|
147
|
+
puts
|
148
|
+
print_table(table_for_summary(name))
|
149
|
+
end
|
150
|
+
|
151
|
+
def print_table_for_summary_duration(name)
|
152
|
+
return if @telemetry.summary(name).empty?
|
153
|
+
|
154
|
+
puts
|
155
|
+
print_table(table_for_summary_durations(name))
|
156
|
+
end
|
157
|
+
|
158
|
+
def print_table(table)
|
159
|
+
lengths = table.transpose.map { |r| r.map(&:size).max }
|
160
|
+
|
161
|
+
print_row(table[0], lengths)
|
162
|
+
|
163
|
+
puts "#{'─' * lengths[0]}─┼─#{lengths[1..-1].map { |length| '─' * length }.join('───')}"
|
164
|
+
|
165
|
+
table[1..-1].each { |row| print_row(row, lengths) }
|
166
|
+
end
|
167
|
+
|
168
|
+
def print_row(row, lengths)
|
169
|
+
values = row.zip(lengths).map { |text, length| text.rjust length }
|
170
|
+
|
171
|
+
puts values[0] + ' │ ' + values[1..-1].join(' ')
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
data/lib/nanoc/filters/xsl.rb
CHANGED
data/lib/nanoc/version.rb
CHANGED
@@ -0,0 +1,54 @@
|
|
1
|
+
describe Nanoc::Int::DirectedGraph do
|
2
|
+
subject(:graph) { described_class.new([1, 2, 3]) }
|
3
|
+
|
4
|
+
describe '#any_cycle' do
|
5
|
+
subject { graph.any_cycle }
|
6
|
+
|
7
|
+
context 'no cycles' do
|
8
|
+
it { is_expected.to be_nil }
|
9
|
+
end
|
10
|
+
|
11
|
+
context 'one cycle without head' do
|
12
|
+
before do
|
13
|
+
graph.add_edge(1, 2)
|
14
|
+
graph.add_edge(2, 1)
|
15
|
+
end
|
16
|
+
|
17
|
+
it { is_expected.to eq([1, 2]) }
|
18
|
+
end
|
19
|
+
|
20
|
+
context 'one cycle with head' do
|
21
|
+
before do
|
22
|
+
graph.add_edge(1, 2)
|
23
|
+
graph.add_edge(2, 3)
|
24
|
+
graph.add_edge(3, 2)
|
25
|
+
end
|
26
|
+
|
27
|
+
it { is_expected.to eq([2, 3]) }
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'large cycle' do
|
31
|
+
before do
|
32
|
+
graph.add_edge(1, 2)
|
33
|
+
graph.add_edge(2, 3)
|
34
|
+
graph.add_edge(3, 4)
|
35
|
+
graph.add_edge(4, 5)
|
36
|
+
graph.add_edge(5, 1)
|
37
|
+
end
|
38
|
+
|
39
|
+
it { is_expected.to eq([1, 2, 3, 4, 5]) }
|
40
|
+
end
|
41
|
+
|
42
|
+
context 'large cycle with head' do
|
43
|
+
before do
|
44
|
+
graph.add_edge(1, 2)
|
45
|
+
graph.add_edge(2, 3)
|
46
|
+
graph.add_edge(3, 4)
|
47
|
+
graph.add_edge(4, 5)
|
48
|
+
graph.add_edge(5, 2)
|
49
|
+
end
|
50
|
+
|
51
|
+
it { is_expected.to eq([2, 3, 4, 5]) }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|