nanoc 4.7.0 → 4.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|