seeing_is_believing 3.6.1 → 4.0.0

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -5
  3. data/Rakefile +2 -0
  4. data/Readme.md +1 -1
  5. data/appveyor.yml +3 -5
  6. data/features/regression.feature +7 -0
  7. data/features/xmpfilter-style.feature +1 -0
  8. data/lib/seeing_is_believing.rb +2 -1
  9. data/lib/seeing_is_believing/binary.rb +1 -1
  10. data/lib/seeing_is_believing/binary/annotate_marked_lines.rb +1 -0
  11. data/lib/seeing_is_believing/binary/comment_lines.rb +1 -1
  12. data/lib/seeing_is_believing/binary/commentable_lines.rb +7 -7
  13. data/lib/seeing_is_believing/binary/config.rb +1 -3
  14. data/lib/seeing_is_believing/binary/remove_annotations.rb +1 -1
  15. data/lib/seeing_is_believing/code.rb +3 -3
  16. data/lib/seeing_is_believing/evaluate_by_moving_files.rb +30 -32
  17. data/lib/seeing_is_believing/event_stream/consumer.rb +2 -0
  18. data/lib/seeing_is_believing/event_stream/events.rb +7 -0
  19. data/lib/seeing_is_believing/event_stream/handlers/record_exit_events.rb +3 -3
  20. data/lib/seeing_is_believing/event_stream/handlers/update_result.rb +2 -1
  21. data/lib/seeing_is_believing/event_stream/producer.rb +14 -0
  22. data/lib/seeing_is_believing/safe.rb +1 -7
  23. data/lib/seeing_is_believing/swap_files.rb +90 -0
  24. data/lib/seeing_is_believing/the_matrix.rb +17 -4
  25. data/lib/seeing_is_believing/version.rb +1 -1
  26. data/lib/seeing_is_believing/wrap_expressions.rb +22 -8
  27. data/lib/seeing_is_believing/wrap_expressions_with_inspect.rb +3 -0
  28. data/seeing_is_believing.gemspec +4 -5
  29. data/spec/binary/config_spec.rb +2 -2
  30. data/spec/evaluate_by_moving_files_spec.rb +18 -17
  31. data/spec/event_stream_spec.rb +1 -0
  32. data/spec/seeing_is_believing_spec.rb +99 -24
  33. data/spec/sib_spec_helpers/version.rb +17 -0
  34. data/spec/spec_helper.rb +2 -18
  35. data/spec/wrap_expressions_spec.rb +9 -3
  36. metadata +26 -12
  37. data/lib/seeing_is_believing/backup_file.rb +0 -50
  38. data/lib/seeing_is_believing/customize_pp.rb +0 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: abaf2ec44d75ad4ca500c71bf90128baea06be5c0bd61fef5a0de81a402fef49
4
- data.tar.gz: b487790b90fb61b27e77fee26622c8210a0b6e435c2084205bb791bcf3e28c08
3
+ metadata.gz: d4cc5ca0f30eacf4bb2729e295c557de2dcc20acb856389dda16f776bb0df2ad
4
+ data.tar.gz: e5711ae5d11fff1d65259beb1f65f9bbf6fba5e2d1494e22394d652fac26dfa6
5
5
  SHA512:
6
- metadata.gz: '03996ee758737b276089eec6037a0c999678097759fc1dac9a1a338eae62327cb5d76253e0c491ae77a7e0852edb104da629e59db1c60228c43076d100ae8215'
7
- data.tar.gz: 915f7e561d0bcb18f863c068e72305fa445f1b4f5f9d390843478a4ddb662cf8c38af388d5cc8da5f9ddaeeba1e569ad803c5d5c5b913d92bc6642e60155484e
6
+ metadata.gz: c00e05b913ad9122b4f2a76146c447868cec4b53dad4f8cc71ee6ef8e0607005c9b6cf8c31bd5e439096ea52ee445f71ff06be44872b65b4e6ce00a3931cfe64
7
+ data.tar.gz: 155ee68e5acaaf7b9d6e813bb3f1710b759c9b093a467c8aeb9d84f4df298716ab1cfba2cab504b75c9b98aad8f6d0d6146ae13be35db1f26dbacfa70e035e14
@@ -1,11 +1,8 @@
1
1
  language: ruby
2
- before_install: gem install bundler
3
2
  install: rake install
4
3
  script: bundle exec rake ci
5
4
  rvm:
6
- - 2.0.0
7
- - 2.1.2
8
- - 2.2.0
9
- - 2.3.1
10
5
  - 2.4.1
11
6
  - 2.5.0
7
+ - 2.6.3
8
+ - 2.7.1
data/Rakefile CHANGED
@@ -42,6 +42,8 @@ task cuke: :bundle do
42
42
  sh 'ruby', '--disable-gem',
43
43
  *require_paths,
44
44
  '-S', 'bundle/bin/cucumber',
45
+ '--quiet', # omit stepdefs, which are usually spammy for me https://twitter.com/josh_cheek/status/1082767053071765504
46
+ '--fail-fast', # stop as soon as we see an error (don't waste CI time, don't make it hard to find error info later)
45
47
  '--tags', '~@not-implemented',
46
48
  '--tags', "~@not-#{RUBY_VERSION}",
47
49
  '--tags', "~@not-#{ruby_version_without_patchlevel}",
data/Readme.md CHANGED
@@ -8,7 +8,7 @@ Evaluates Ruby code, recording the results of each line.
8
8
  Integrates with any extensible editor (I've integrated it with many already, see [the list](https://github.com/JoshCheek/seeing_is_believing/wiki/Editor-Integration)).
9
9
  If you like Swift Playgrounds, you'll like SiB.
10
10
 
11
- ![example](https://s3.amazonaws.com/josh.cheek/images/scratch/sib-example1.gif)
11
+ ![example](docs/example.gif)
12
12
 
13
13
 
14
14
  Helpful links
@@ -26,8 +26,6 @@ test_script:
26
26
 
27
27
  environment:
28
28
  matrix:
29
- - ruby_version: '21'
30
- - ruby_version: '22'
31
- - ruby_version: '23'
32
- # - ruby_version: '24'
33
- # - ruby_version: '25'
29
+ - ruby_version: '24'
30
+ - ruby_version: '25'
31
+ - ruby_version: '26'
@@ -267,6 +267,8 @@ Feature:
267
267
  """
268
268
 
269
269
 
270
+ # ChildProcess doesn't seem to handle this well in Windows. IDK.
271
+ @not-windows
270
272
  Scenario: Long DATA segment in a valid file
271
273
  Given the file "long_valid_data_segment.rb":
272
274
  """
@@ -434,6 +436,9 @@ Feature:
434
436
  """
435
437
 
436
438
 
439
+ # 2.4 and 2.5 don't correclty detect deadlock on Windows: https://bugs.ruby-lang.org/issues/16110
440
+ @not-2.4
441
+ @not-2.5
437
442
  Scenario: Deadlocked
438
443
  Given the file "deadlocked.rb":
439
444
  """
@@ -472,6 +477,8 @@ Feature:
472
477
  # but just generally that it doesn't fkn blow up
473
478
  @not-2.4
474
479
  @not-2.5
480
+ @not-2.6
481
+ @not-2.7
475
482
  Scenario: Old JSON bug
476
483
  Given the file "json_and_encodings.rb":
477
484
  """
@@ -72,6 +72,7 @@ Feature: Xmpfilter style
72
72
  # :wibble=>{:magic_word=>"xyzzy"}}
73
73
  """
74
74
 
75
+ @not-2.3
75
76
  Scenario: --xmpfilter-style, when displayed on the next line, prints the string across multiple lines
76
77
  Given the file "xmpfilter-prev-line-is-multiline-string.rb":
77
78
  """
@@ -43,8 +43,9 @@ class SeeingIsBelieving
43
43
  options.debugger.context("REWRITTEN PROGRAM") { new_program }
44
44
 
45
45
  EvaluateByMovingFiles.call \
46
- new_program,
47
46
  filename,
47
+ @program,
48
+ new_program,
48
49
  event_handler: event_handler(options.debugger, options.event_handler),
49
50
  provided_input: options.stdin,
50
51
  require_files: options.require_files,
@@ -23,7 +23,7 @@ class SeeingIsBelieving
23
23
  end
24
24
 
25
25
  if config.errors.any?
26
- stderr.puts *config.errors, *config.deprecations
26
+ stderr.puts(*config.errors, *config.deprecations)
27
27
  return NONDISPLAYABLE_ERROR_STATUS
28
28
  end
29
29
 
@@ -46,6 +46,7 @@ class SeeingIsBelieving
46
46
  should_pp = false
47
47
  WrapExpressions.call \
48
48
  program,
49
+ before_all: -> { "BEGIN { $SiB.file_loaded };" },
49
50
  before_each: -> line_number {
50
51
  inspect = "$SiB.record_result(:inspect, #{line_number}, ("
51
52
  pp = "$SiB.record_result(:pp, #{line_number}, ("
@@ -17,7 +17,7 @@ class SeeingIsBelieving
17
17
  def call
18
18
  @call ||= begin
19
19
  commentable_lines = CommentableLines.new raw_code
20
- commentable_lines.call.each do |line_number, (index_of_newline, col)|
20
+ commentable_lines.call.each do |line_number, (index_of_newline, _col)|
21
21
  first_index = last_index = index_of_newline
22
22
  first_index -= 1 while first_index > 0 && raw_code[first_index-1] != "\n"
23
23
  comment_text = commenter.call raw_code[first_index...last_index], line_number
@@ -54,13 +54,13 @@ class SeeingIsBelieving
54
54
  def remove_lines_whose_newline_is_escaped(line_num_to_location)
55
55
  ors_indexes = code_obj.indexes_of_ors_at_eol
56
56
  line_num_to_location
57
- .select { |line_number, (index_of_newline, col)|
57
+ .select { |line_number, (index_of_newline, _col)|
58
58
  code[index_of_newline-1] == '\\'
59
59
  }
60
- .reject { |line_number, (index_of_newline, col)|
60
+ .reject { |line_number, (index_of_newline, _col)|
61
61
  ors_indexes.include? index_of_newline
62
62
  }
63
- .each { |line_number, (index_of_newline, col)|
63
+ .each { |line_number, (_index_of_newline, _col)|
64
64
  line_num_to_location.delete line_number
65
65
  }
66
66
  end
@@ -73,8 +73,8 @@ class SeeingIsBelieving
73
73
  begin_pos = comment.location.expression.begin_pos
74
74
  end_pos = comment.location.expression.end_pos
75
75
  range = begin_pos...end_pos
76
- line_num_to_location.select { |line_number, (index_of_newline, col)| range.include? index_of_newline }
77
- .each { |line_number, (index_of_newline, col)| line_num_to_location.delete line_number }
76
+ line_num_to_location.select { |line_number, (index_of_newline, _col)| range.include? index_of_newline }
77
+ .each { |line_number, (_index_of_newline, _col)| line_num_to_location.delete line_number }
78
78
  end
79
79
  end
80
80
  end
@@ -82,8 +82,8 @@ class SeeingIsBelieving
82
82
  def remove_lines_inside_of_strings_and_things(line_num_to_location, ast)
83
83
  invalid_boundaries = ranges_of_atomic_expressions ast, []
84
84
  invalid_boundaries.each do |invalid_boundary|
85
- line_num_to_location.select { |line_number, (index_of_newline, col)| invalid_boundary.include? index_of_newline }
86
- .each { |line_number, (index_of_newline, col)| line_num_to_location.delete line_number }
85
+ line_num_to_location.select { |line_number, (index_of_newline, _col)| invalid_boundary.include? index_of_newline }
86
+ .each { |line_number, (_index_of_newline, _col)| line_num_to_location.delete line_number }
87
87
  end
88
88
  end
89
89
 
@@ -108,7 +108,6 @@ class SeeingIsBelieving
108
108
  self.lib_options.rewrite_code = AnnotateMarkedLines.code_rewriter(markers)
109
109
  self.remove_value_prefixes = false
110
110
  self.lib_options.require_files << 'pp'
111
- self.lib_options.require_files << 'seeing_is_believing/customize_pp'
112
111
 
113
112
  when '--toggle-mark'
114
113
  extract_positive_int_for.call arg do |n|
@@ -313,8 +312,7 @@ class SeeingIsBelieving
313
312
  end
314
313
 
315
314
  def Binary.help_screen(markers)
316
- value = markers[:value][:prefix]
317
- stdout = markers[:stdout][:prefix]
315
+ value = markers[:value][:prefix]
318
316
 
319
317
  <<FLAGS
320
318
  Usage: seeing_is_believing [options] [filename]
@@ -76,7 +76,7 @@ class SeeingIsBelieving
76
76
  [annotation, comment]
77
77
  }
78
78
  .slice_before { |annotation, comment| annotation } # annotations begin chunks
79
- .select { |(annotation, start), *| annotation } # discard chunks not beginning with an annotation (probably can only happens on first comment)
79
+ .select { |(annotation, _start), *| annotation } # discard chunks not beginning with an annotation (probably can only happens on first comment)
80
80
  .map { |(annotation, start), *rest| # end the chunk if the comment doesn't meet nextline criteria
81
81
  nextline_comments = []
82
82
  prev = start
@@ -91,8 +91,8 @@ class SeeingIsBelieving
91
91
  def indexes_of_ors_at_eol
92
92
  Set.new(
93
93
  @tokens.select { |type, *| type == :tGVAR }
94
- .select { |_, (var, range)| var == '$\\'.freeze }
95
- .map { |_, (var, range)| range.end_pos }
94
+ .select { |_, (var, _range)| var == '$\\'.freeze }
95
+ .map { |_, (_var, range)| range.end_pos }
96
96
  )
97
97
  end
98
98
 
@@ -106,7 +106,7 @@ class SeeingIsBelieving
106
106
 
107
107
  def body_range_from_tokens(tokens)
108
108
  return range_for(0, 0) if raw.start_with? "__END__\n"
109
- (name, (_, range)) = tokens.max_by { |name, (data, range)| range.end_pos } ||
109
+ (name, (_, range)) = tokens.max_by { |name, (_data, range)| range.end_pos } ||
110
110
  [nil, [nil, range_for(0, 1)]]
111
111
  end_pos = range.end_pos
112
112
  end_pos += 1 if name == :tCOMMENT || name == :tSTRING_END || name == :tSEMI
@@ -17,8 +17,9 @@ require "childprocess"
17
17
 
18
18
  require 'seeing_is_believing/result'
19
19
  require 'seeing_is_believing/debugger'
20
- require 'seeing_is_believing/backup_file'
20
+ require 'seeing_is_believing/swap_files'
21
21
  require 'seeing_is_believing/event_stream/consumer'
22
+ require 'seeing_is_believing/event_stream/events'
22
23
 
23
24
  ChildProcess.posix_spawn = true # forking locks up for some reason when we run SiB inside of SiB
24
25
 
@@ -28,13 +29,14 @@ class SeeingIsBelieving
28
29
  new(*args).call
29
30
  end
30
31
 
31
- attr_accessor :program, :provided_input, :require_flags, :load_path_flags, :encoding, :timeout_seconds, :debugger, :event_handler, :max_line_captures
32
+ attr_accessor :user_program, :rewritten_program, :provided_input, :require_flags, :load_path_flags, :encoding, :timeout_seconds, :debugger, :event_handler, :max_line_captures
32
33
 
33
- attr_accessor :file_directory, :file_path, :local_cwd, :relative_filename, :backup_filename
34
+ attr_accessor :file_directory, :file_path, :local_cwd, :relative_filename, :backup_path
34
35
 
35
- def initialize(program, file_path, options={})
36
+ def initialize(file_path, user_program, rewritten_program, options={})
36
37
  options = options.dup
37
- self.program = program
38
+ self.user_program = user_program
39
+ self.rewritten_program = rewritten_program
38
40
  self.encoding = options.delete(:encoding) || "u"
39
41
  self.timeout_seconds = options.delete(:timeout_seconds) || 0 # 0 is the new infinity
40
42
  self.provided_input = options.delete(:provided_input) || String.new
@@ -47,26 +49,20 @@ class SeeingIsBelieving
47
49
  self.file_directory = File.dirname file_path
48
50
  file_name = File.basename file_path
49
51
  self.relative_filename = local_cwd ? file_name : file_path
50
- self.backup_filename = File.join file_directory, "seeing_is_believing_backup.#{file_name}"
52
+ self.backup_path = File.join file_directory, "seeing_is_believing_backup.#{file_name}"
51
53
 
52
54
  options.any? && raise(ArgumentError, "Unknown options: #{options.inspect}")
53
55
  end
54
56
 
55
57
  def call
56
- BackupFile.call file_path, backup_filename do
57
- write_program_to_file
58
- evaluate_file
58
+ SwapFiles.call file_path, backup_path, user_program, rewritten_program do |swap_files|
59
+ evaluate_file swap_files
59
60
  end
60
61
  end
61
62
 
62
63
  private
63
64
 
64
- def write_program_to_file
65
- File.open(file_path, 'w', external_encoding: "utf-8") { |f| f.write program.to_s }
66
- end
67
-
68
-
69
- def evaluate_file
65
+ def evaluate_file(swap_files)
70
66
  event_server = TCPServer.new(0) # dynamically allocates an available port
71
67
 
72
68
  # setup streams
@@ -78,7 +74,7 @@ class SeeingIsBelieving
78
74
  [Marshal.dump(
79
75
  event_stream_port: event_server.addr[1],
80
76
  max_line_captures: max_line_captures,
81
- num_lines: program.lines.count,
77
+ num_lines: user_program.lines.count,
82
78
  filename: relative_filename,
83
79
  )].pack('m0')
84
80
 
@@ -113,7 +109,12 @@ class SeeingIsBelieving
113
109
 
114
110
  # set up the event consumer
115
111
  consumer = EventStream::Consumer.new(events: eventstream, stdout: stdout, stderr: stderr)
116
- consumer_thread = Thread.new { consumer.each { |e| event_handler.call e } }
112
+ consumer_thread = Thread.new do
113
+ consumer.each do |e|
114
+ swap_files.show_user_program if e.is_a? SeeingIsBelieving::EventStream::Events::FileLoaded
115
+ event_handler.call e
116
+ end
117
+ end
117
118
 
118
119
  # wait for completion
119
120
  if timeout_seconds == 0
@@ -131,13 +132,19 @@ class SeeingIsBelieving
131
132
  # On Windows, we need to call stop if there is an error since it interrupted
132
133
  # the previos waiting/polling. If we don't call stop, in that situation, it will
133
134
  # leave orphan processes. On Unix, we need to always call stop or it may leave orphans
134
- if ChildProcess.unix?
135
- child.stop
136
- elsif $!
137
- child.stop
138
- consumer.process_exitstatus(child.exit_code)
135
+ begin
136
+ if ChildProcess.unix?
137
+ child.stop
138
+ elsif $!
139
+ child.stop
140
+ consumer.process_exitstatus(child.exit_code)
141
+ end
142
+ child.alive? && child.stop
143
+ rescue ChildProcess::Error
144
+ # On AppVeyor, I keep getting errors
145
+ # The handle is invalid: https://ci.appveyor.com/project/JoshCheek/seeing-is-believing/build/22
146
+ # Access is denied: https://ci.appveyor.com/project/JoshCheek/seeing-is-believing/build/24
139
147
  end
140
- cleanup_run(child)
141
148
  close_streams(stdout, stderr, eventstream, event_server)
142
149
  end
143
150
 
@@ -154,14 +161,5 @@ class SeeingIsBelieving
154
161
  def close_streams(*streams)
155
162
  streams.each { |io| io.close unless io.closed? }
156
163
  end
157
-
158
- # On AppVeyor, I keep getting errors
159
- # The handle is invalid: https://ci.appveyor.com/project/JoshCheek/seeing-is-believing/build/22
160
- # Access is denied: https://ci.appveyor.com/project/JoshCheek/seeing-is-believing/build/24
161
- def cleanup_run(child, *streams)
162
- child.alive? && child.stop
163
- rescue ChildProcess::Error
164
- # noop
165
- end
166
164
  end
167
165
  end
@@ -208,6 +208,8 @@ class SeeingIsBelieving
208
208
  Events::RubyVersion.new(value: shift_string(line))
209
209
  when :filename
210
210
  Events::Filename.new(value: shift_string(line))
211
+ when :file_loaded
212
+ Events::FileLoaded.new
211
213
  when :exec
212
214
  Events::Exec.new(args: shift_string(line))
213
215
  else
@@ -58,6 +58,13 @@ class SeeingIsBelieving
58
58
  attributes :value
59
59
  end
60
60
 
61
+ # For knowing when you can perform certain tasks (eg moving the file back)
62
+ class FileLoaded < Event
63
+ def self.event_name
64
+ :file_loaded
65
+ end
66
+ end
67
+
61
68
  # Number of lines in the program.
62
69
  class NumLines < Event
63
70
  def self.event_name
@@ -7,8 +7,8 @@ class SeeingIsBelieving
7
7
  attr_reader :exitstatus
8
8
  attr_reader :timeout_seconds
9
9
 
10
- def initialize(next_observer)
11
- @next_observer = next_observer
10
+ def initialize(next_handler)
11
+ @next_handler = next_handler
12
12
  end
13
13
 
14
14
  def call(event)
@@ -18,7 +18,7 @@ class SeeingIsBelieving
18
18
  when Events::Timeout
19
19
  @timeout_seconds = event.seconds
20
20
  end
21
- @next_observer.call(event)
21
+ @next_handler.call(event)
22
22
  end
23
23
  end
24
24
  end
@@ -29,7 +29,8 @@ class SeeingIsBelieving
29
29
  Finished,
30
30
  StdoutClosed,
31
31
  StderrClosed,
32
- EventStreamClosed
32
+ EventStreamClosed,
33
+ FileLoaded
33
34
  # no op
34
35
  else raise "Unknown event: #{event.inspect}"
35
36
  end
@@ -7,6 +7,16 @@ using SeeingIsBelieving::Safe
7
7
  class SeeingIsBelieving
8
8
  module EventStream
9
9
  class Producer
10
+
11
+ # Guarding against hostile users (e.g. me) that do ridiculous things like blowing away these constants
12
+ old_w, $-w = $-w, nil # Ruby warns about accessing deprecated constants
13
+ Object.constants.each do |name|
14
+ const_set name, Object.const_get(name)
15
+ end
16
+ $-w = old_w
17
+
18
+ ErrnoEPIPE = Errno::EPIPE # not actually tested, but we can see it is referenced below
19
+
10
20
  module NullQueue
11
21
  extend self
12
22
  Queue.instance_methods(false).each do |name|
@@ -40,6 +50,10 @@ class SeeingIsBelieving
40
50
  queue << "max_line_captures #{max_line_captures}"
41
51
  end
42
52
 
53
+ def file_loaded
54
+ queue << "file_loaded"
55
+ end
56
+
43
57
  StackErrors = [SystemStackError]
44
58
  StackErrors << Java::JavaLang::StackOverflowError if defined?(RUBY_PLATFORM) && RUBY_PLATFORM == 'java'
45
59
  def record_result(type, line_number, value)
@@ -50,13 +50,7 @@ class SeeingIsBelieving
50
50
  alias to_str to_str
51
51
  end
52
52
 
53
- # in 2.4 we should use Integer instead, but it's not obvious to me how
54
- # to detect this. eg defined?(Fixnum) returns "constant". Accessing it
55
- # leads to a warning, but SiB turns warnings off so you don't see it.
56
- # So.... for now, it incidentally doesn't do anything annoying, but would
57
- # be good to figure out something better (eg if we ever wanted to allow the
58
- # user to decide whether warnings should display or not)
59
- refine Fixnum do
53
+ refine Integer do
60
54
  alias to_s to_s
61
55
  alias next next
62
56
  alias < <
@@ -0,0 +1,90 @@
1
+ require 'seeing_is_believing/error'
2
+ require 'seeing_is_believing/hard_core_ensure'
3
+
4
+ class SeeingIsBelieving
5
+ class SwapFiles
6
+ # Might honeslty make more sense to break this out into 2 different classes.
7
+ # We've got to do some confusing state accounting since there are really 2
8
+ # algorithms here:
9
+ #
10
+ # if the file exists:
11
+ # make sure there isn't a backup (could cause the user to lose their file)
12
+ # back the file up
13
+ # write the rewritten code to the file
14
+ # if we are told to show the user program
15
+ # move the backup over the top of the rewritten file
16
+ # do nothing in the ensure block
17
+ # else
18
+ # in the ensure block: move the backup over the top of the rewritten file
19
+ # if the file DNE:
20
+ # write the rewritten code to the file
21
+ # if we are told to show the user program
22
+ # write the user program to the file
23
+ # delete the file in the ensure block
24
+ def self.call(*args, &block)
25
+ new(*args, &block).call
26
+ end
27
+
28
+ def initialize(file_path, backup_path, user_program, rewritten_program, &block)
29
+ self.file_path = file_path
30
+ self.block = block
31
+ self.backup_path = backup_path
32
+ self.user_program = user_program
33
+ self.rewritten_program = rewritten_program
34
+ end
35
+
36
+ def call
37
+ HardCoreEnsure.call \
38
+ code: -> {
39
+ File.exist? backup_path and
40
+ raise TempFileAlreadyExists.new(file_path, backup_path)
41
+
42
+ @has_file = File.exist? file_path
43
+
44
+ if @has_file
45
+ File.rename file_path, backup_path
46
+ @needs_restore = true
47
+ end
48
+
49
+ save_file rewritten_program
50
+
51
+ block.call self
52
+ },
53
+ ensure: -> {
54
+ set_back_to_initial_conditions
55
+ }
56
+ end
57
+
58
+ def show_user_program
59
+ if @needs_restore
60
+ restore
61
+ else
62
+ save_file user_program
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ attr_accessor :block, :file_path, :backup_path, :rewritten_program, :user_program
69
+
70
+ def restore
71
+ File.rename(backup_path, file_path)
72
+ @needs_restore = false
73
+ end
74
+
75
+
76
+ def save_file(program)
77
+ File.open file_path, 'w', external_encoding: "utf-8" do |f|
78
+ f.write program.to_s
79
+ end
80
+ end
81
+
82
+ def set_back_to_initial_conditions
83
+ if @needs_restore
84
+ restore
85
+ elsif !@has_file
86
+ File.delete(file_path)
87
+ end
88
+ end
89
+ end
90
+ end