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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +4 -4
  3. data/NEWS.md +12 -1
  4. data/README.md +1 -1
  5. data/lib/nanoc/base/entities/directed_graph.rb +16 -0
  6. data/lib/nanoc/base/entities/identifiable_collection.rb +24 -8
  7. data/lib/nanoc/base/entities/outdatedness_reasons.rb +5 -0
  8. data/lib/nanoc/base/errors.rb +11 -6
  9. data/lib/nanoc/base/memoization.rb +6 -23
  10. data/lib/nanoc/base/services/compiler/phases/abstract.rb +3 -3
  11. data/lib/nanoc/base/services/compiler/phases/cache.rb +1 -3
  12. data/lib/nanoc/base/services/compiler/phases/mark_done.rb +1 -3
  13. data/lib/nanoc/base/services/compiler/phases/recalculate.rb +1 -3
  14. data/lib/nanoc/base/services/compiler/phases/resume.rb +1 -3
  15. data/lib/nanoc/base/services/compiler/phases/write.rb +1 -3
  16. data/lib/nanoc/base/services/filter.rb +15 -0
  17. data/lib/nanoc/base/services/item_rep_selector.rb +1 -1
  18. data/lib/nanoc/base/services/outdatedness_checker.rb +3 -1
  19. data/lib/nanoc/base/services/outdatedness_rule.rb +7 -0
  20. data/lib/nanoc/base/services/outdatedness_rules.rb +16 -0
  21. data/lib/nanoc/cli/commands/compile.rb +12 -411
  22. data/lib/nanoc/cli/commands/compile_listeners/abstract.rb +30 -0
  23. data/lib/nanoc/cli/commands/compile_listeners/debug_printer.rb +34 -0
  24. data/lib/nanoc/cli/commands/compile_listeners/diff_generator.rb +91 -0
  25. data/lib/nanoc/cli/commands/compile_listeners/file_action_printer.rb +61 -0
  26. data/lib/nanoc/cli/commands/compile_listeners/stack_prof_profiler.rb +22 -0
  27. data/lib/nanoc/cli/commands/compile_listeners/timing_recorder.rb +174 -0
  28. data/lib/nanoc/filters/xsl.rb +2 -0
  29. data/lib/nanoc/version.rb +1 -1
  30. data/spec/nanoc/base/directed_graph_spec.rb +54 -0
  31. data/spec/nanoc/base/errors/dependency_cycle_spec.rb +32 -0
  32. data/spec/nanoc/base/filter_spec.rb +36 -0
  33. data/spec/nanoc/base/services/compiler/phases/abstract_spec.rb +23 -11
  34. data/spec/nanoc/base/services/outdatedness_rules_spec.rb +41 -0
  35. data/spec/nanoc/cli/commands/compile/file_action_printer_spec.rb +1 -1
  36. data/spec/nanoc/cli/commands/compile/timing_recorder_spec.rb +124 -54
  37. data/spec/nanoc/cli/commands/compile_spec.rb +1 -1
  38. data/spec/nanoc/regressions/gh_1040_spec.rb +1 -1
  39. data/spec/nanoc/regressions/gh_924_spec.rb +89 -0
  40. data/test/base/test_compiler.rb +1 -1
  41. data/test/base/test_memoization.rb +6 -0
  42. data/test/cli/commands/test_compile.rb +2 -2
  43. 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
@@ -5,6 +5,8 @@ module Nanoc::Filters
5
5
 
6
6
  requires 'nokogiri'
7
7
 
8
+ always_outdated
9
+
8
10
  # Runs the item content through an [XSLT](http://www.w3.org/TR/xslt)
9
11
  # stylesheet using [Nokogiri](http://nokogiri.org/).
10
12
  #
data/lib/nanoc/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  module Nanoc
2
2
  # The current Nanoc version.
3
- VERSION = '4.7.0'.freeze
3
+ VERSION = '4.7.1'.freeze
4
4
  end
@@ -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