rundoc 1.0.0 → 1.0.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +7 -0
  3. data/CHANGELOG.md +10 -0
  4. data/README.md +219 -96
  5. data/bin/rundoc +4 -68
  6. data/lib/rundoc.rb +1 -0
  7. data/lib/rundoc/cli.rb +84 -0
  8. data/lib/rundoc/code_command.rb +3 -2
  9. data/lib/rundoc/code_command/background/process_spawn.rb +28 -2
  10. data/lib/rundoc/code_command/background/start.rb +1 -0
  11. data/lib/rundoc/code_command/bash/cd.rb +20 -2
  12. data/lib/rundoc/code_command/no_such_command.rb +4 -0
  13. data/lib/rundoc/code_command/rundoc/depend_on.rb +37 -0
  14. data/lib/rundoc/code_command/rundoc/require.rb +41 -0
  15. data/lib/rundoc/code_command/rundoc_command.rb +4 -1
  16. data/lib/rundoc/code_command/website.rb +7 -0
  17. data/lib/rundoc/code_command/website/driver.rb +111 -0
  18. data/lib/rundoc/code_command/website/navigate.rb +29 -0
  19. data/lib/rundoc/code_command/website/screenshot.rb +28 -0
  20. data/lib/rundoc/code_command/website/visit.rb +47 -0
  21. data/lib/rundoc/code_section.rb +13 -4
  22. data/lib/rundoc/parser.rb +4 -3
  23. data/lib/rundoc/peg_parser.rb +1 -1
  24. data/lib/rundoc/version.rb +1 -1
  25. data/rundoc.gemspec +6 -2
  26. data/test/fixtures/build_logs/rundoc.md +56 -0
  27. data/test/fixtures/depend_on/dependency/rundoc.md +5 -0
  28. data/test/fixtures/depend_on/main/rundoc.md +10 -0
  29. data/test/fixtures/java/rundoc.md +9 -0
  30. data/test/fixtures/rails_4/rundoc.md +1 -1
  31. data/test/fixtures/rails_5/rundoc.md +3 -3
  32. data/test/fixtures/{rails_5_beta → rails_6}/rundoc.md +79 -86
  33. data/test/fixtures/require/dependency/rundoc.md +5 -0
  34. data/test/fixtures/require/main/rundoc.md +10 -0
  35. data/test/fixtures/screenshot/rundoc.md +10 -0
  36. data/test/rundoc/peg_parser_test.rb +33 -0
  37. metadata +71 -8
data/bin/rundoc CHANGED
@@ -15,12 +15,11 @@ $: << File.expand_path(File.dirname(File.realpath(__FILE__)) + '/../lib')
15
15
  require 'rundoc'
16
16
  require 'thor'
17
17
 
18
- class RundocCLI < Thor
18
+ class RundocThorCLI < Thor
19
19
 
20
20
  def initialize(*args)
21
21
  super
22
- @path = options[:path]
23
- @working_dir = Pathname.new(File.expand_path("../", @path))
22
+ @path = options[:path]
24
23
  end
25
24
 
26
25
  default_task :help
@@ -28,71 +27,8 @@ class RundocCLI < Thor
28
27
  desc "build", "turns rundoc file into docs and a project"
29
28
  class_option :path, banner: "path/to/file.md", optional: true, default: 'rundoc.md'
30
29
  def build
31
- raise "#{@path} does not exist" unless File.exist?(@path)
32
- raise "Expecting #{@path} to be a rundoc markdown file" unless File.file?(@path)
33
- source_contents = File.read(@path)
34
- tmp_dir = @working_dir.join("tmp")
35
-
36
- FileUtils.remove_entry_secure(tmp_dir) if tmp_dir.exist?
37
- tmp_dir.mkdir
38
- banner = <<~HEREDOC
39
- <!-- STOP
40
- This file was generated by a rundoc script, do not modify it.
41
-
42
- Instead modify the rundoc script and re-run it.
43
-
44
- Command: #{ $0 } #{$*.join(' ')}
45
- STOP -->
46
- HEREDOC
47
-
48
- puts "== Running your docs"
49
- Dir.chdir(tmp_dir) do
50
- @output = Rundoc::Parser.new(source_contents, Rundoc.parser_options).to_md
51
- Rundoc.sanitize(@output)
52
- @output = "#{banner}\n#{@output}"
53
- end
54
-
55
- puts "== Done, run was successful"
56
- project_name = if Rundoc.project_root
57
- Rundoc.project_root.split('/').last
58
- else
59
- 'project'
60
- end
61
-
62
- project_dir = @working_dir.join(project_name)
63
-
64
- FileUtils.remove_entry_secure(project_dir) if project_dir.exist?
65
-
66
- cp_root = if Rundoc.project_root
67
- tmp_dir.join(Rundoc.project_root, ".")
68
- else
69
- tmp_dir.join(".")
70
- end
71
-
72
- FileUtils.cp_r(cp_root, project_dir)
73
-
74
- FileUtils.remove_entry_secure(tmp_dir) if tmp_dir.exist?
75
-
76
- source_path = project_dir.join("README.md")
77
- puts "== Done, writing original source to #{source_path}"
78
- File.open(source_path, "w") { |f| f.write @output }
79
-
80
- puts "== Copying source"
81
- source_path = project_dir.join("coppied-#{@path.split('/').last}")
82
- File.open(source_path, "w") { |f| f.write source_contents }
83
-
84
- Dir.chdir(project_dir) do
85
- Rundoc.run_after_build
86
- end
87
-
88
- ensure
89
- Rundoc::CodeCommand::Background::ProcessSpawn.tasks.each do |name, task|
90
- next unless task.alive?
91
-
92
- puts "Warning background task is still running, cleaning up: name: #{name}"
93
- task.stop
94
- end
30
+ Rundoc::CLI.new.build(path: @path)
95
31
  end
96
32
  end
97
33
 
98
- RundocCLI.start(ARGV)
34
+ RundocThorCLI.start(ARGV)
@@ -87,3 +87,4 @@ require 'rundoc/parser'
87
87
  require 'rundoc/code_section'
88
88
  require 'rundoc/code_command'
89
89
  require 'rundoc/peg_parser'
90
+ require 'rundoc/cli'
@@ -0,0 +1,84 @@
1
+ module Rundoc
2
+ class CLI
3
+ def build(path: )
4
+ @path = Pathname.new(path).expand_path
5
+ raise "#{@path} does not exist" unless File.exist?(@path)
6
+ raise "Expecting #{@path} to be a rundoc markdown file" unless File.file?(@path)
7
+ @working_dir = Pathname.new(File.expand_path("../", @path))
8
+
9
+
10
+ dot_env_path = File.expand_path("../.env", @path)
11
+ if File.exist?(dot_env_path)
12
+ require 'dotenv'
13
+ Dotenv.load(dot_env_path)
14
+ ENV['AWS_ACCESS_KEY_ID'] ||= ENV['BUCKETEER_AWS_ACCESS_KEY_ID']
15
+ ENV['AWS_REGION'] ||= ENV['BUCKETEER_AWS_REGION']
16
+ ENV['AWS_SECRET_ACCESS_KEY'] ||= ENV['BUCKETEER_AWS_SECRET_ACCESS_KEY']
17
+ ENV['AWS_BUCKET_NAME'] ||= ENV['BUCKETEER_BUCKET_NAME']
18
+ end
19
+
20
+ source_contents = File.read(@path)
21
+ tmp_dir = @working_dir.join("tmp")
22
+
23
+ FileUtils.remove_entry_secure(tmp_dir) if tmp_dir.exist?
24
+ tmp_dir.mkdir
25
+ banner = <<~HEREDOC
26
+ <!-- STOP
27
+ This file was generated by a rundoc script, do not modify it.
28
+
29
+ Instead modify the rundoc script and re-run it.
30
+
31
+ Command: #{ $0 } #{$*.join(' ')}
32
+ STOP -->
33
+ HEREDOC
34
+
35
+ puts "== Running your docs"
36
+ Dir.chdir(tmp_dir) do
37
+ @output = Rundoc::Parser.new(source_contents, document_path: @path).to_md
38
+ Rundoc.sanitize(@output)
39
+ @output = "#{banner}\n#{@output}"
40
+ end
41
+
42
+ puts "== Done, run was successful"
43
+ project_name = if Rundoc.project_root
44
+ Rundoc.project_root.split('/').last
45
+ else
46
+ 'project'
47
+ end
48
+
49
+ project_dir = @working_dir.join(project_name)
50
+
51
+ FileUtils.remove_entry_secure(project_dir) if project_dir.exist?
52
+
53
+ cp_root = if Rundoc.project_root
54
+ tmp_dir.join(Rundoc.project_root, ".")
55
+ else
56
+ tmp_dir.join(".")
57
+ end
58
+
59
+ FileUtils.cp_r(cp_root, project_dir)
60
+
61
+ FileUtils.remove_entry_secure(tmp_dir) if tmp_dir.exist?
62
+
63
+ source_path = project_dir.join("README.md")
64
+ puts "== Done, writing original source to #{source_path}"
65
+ File.open(source_path, "w") { |f| f.write @output }
66
+
67
+ puts "== Copying source"
68
+ source_path = project_dir.join("coppied-#{@path.to_s.split('/').last}")
69
+ File.open(source_path, "w") { |f| f.write source_contents }
70
+
71
+ Dir.chdir(project_dir) do
72
+ Rundoc.run_after_build
73
+ end
74
+
75
+ ensure
76
+ Rundoc::CodeCommand::Background::ProcessSpawn.tasks.each do |name, task|
77
+ next unless task.alive?
78
+
79
+ puts "Warning background task is still running, cleaning up: name: #{name}"
80
+ task.stop
81
+ end
82
+ end
83
+ end
84
+ end
@@ -29,12 +29,12 @@ module Rundoc
29
29
  # Executes command to build project
30
30
  # Is expected to return the result of the command
31
31
  def call(env = {})
32
- raise "not implemented"
32
+ raise "not implemented on #{self.inspect}"
33
33
  end
34
34
 
35
35
  # the output of the command, i.e. `$ cat foo.txt`
36
36
  def to_md(env = {})
37
- raise "not implemented"
37
+ raise "not implemented on #{self.inspect}"
38
38
  end
39
39
  end
40
40
  end
@@ -48,3 +48,4 @@ require 'rundoc/code_command/rundoc_command'
48
48
  require 'rundoc/code_command/no_such_command'
49
49
  require 'rundoc/code_command/raw'
50
50
  require 'rundoc/code_command/background'
51
+ require 'rundoc/code_command/website'
@@ -1,7 +1,32 @@
1
1
  require 'shellwords'
2
2
  require 'timeout'
3
+ require 'fileutils'
3
4
 
4
5
  class Rundoc::CodeCommand::Background
6
+ # This class is responsible for running processes in the background
7
+ #
8
+ # By default it logs output to a file. This can be used to "wait" for a
9
+ # specific output before continuing:
10
+ #
11
+ # server = ProcessSpawn("rails server")
12
+ # server.wait("Use Ctrl-C to stop")
13
+ #
14
+ # The process can be queried for it's status to check if it is still booted or not.
15
+ # the process can also be manually stopped:
16
+ #
17
+ # server = ProcessSpawn("rails server")
18
+ # server.alive? # => true
19
+ # server.stop
20
+ # server.alive? # => false
21
+ #
22
+ #
23
+ # There are class level methods that can be used to "name" and record
24
+ # background processes. They can be used like this:
25
+ #
26
+ # server = ProcessSpawn("rails server")
27
+ # ProcessSpawn.add("muh_server", server)
28
+ # ProcessSpawn.find("muh_server") # => <# ProcessSpawn instance >
29
+ # ProcessSpawn.find("foo") # => RuntimeError "Could not find task with name 'foo', ..."
5
30
  class ProcessSpawn
6
31
  def self.tasks
7
32
  @tasks
@@ -27,6 +52,7 @@ class Rundoc::CodeCommand::Background
27
52
 
28
53
  @log = Pathname.new(@log)
29
54
  @log.dirname.mkpath
55
+ FileUtils.touch(@log)
30
56
 
31
57
  @command = "/usr/bin/env bash -c #{@command.shellescape} >> #{@log} #{out}"
32
58
  @pid = nil
@@ -55,7 +81,7 @@ class Rundoc::CodeCommand::Background
55
81
 
56
82
  def stop
57
83
  return unless alive?
58
- Process.kill('TERM', @pid)
84
+ Process.kill('TERM', -Process.getpgid(@pid))
59
85
  Process.wait(@pid)
60
86
  end
61
87
 
@@ -64,7 +90,7 @@ class Rundoc::CodeCommand::Background
64
90
  end
65
91
 
66
92
  private def call
67
- @pid ||= Process.spawn(@command)
93
+ @pid ||= Process.spawn(@command, pgroup: true)
68
94
  end
69
95
  end
70
96
  end
@@ -7,6 +7,7 @@ class Rundoc::CodeCommand::Background
7
7
  @name = name
8
8
  @wait = wait
9
9
  @allow_fail = allow_fail
10
+ FileUtils.touch(log)
10
11
 
11
12
  @spawn = ProcessSpawn.new(
12
13
  @command,
@@ -8,11 +8,29 @@ class Rundoc::CodeCommand::Bash
8
8
  @line = line
9
9
  end
10
10
 
11
+ # Ignore duplicate chdir warnings "warning: conflicting chdir during another chdir block"
12
+ def supress_chdir_warning
13
+ old_stderr = $stderr
14
+ capture_stderr = StringIO.new
15
+ $stderr = capture_stderr
16
+ return yield
17
+ ensure
18
+ if old_stderr
19
+ $stderr = old_stderr
20
+ capture_string = capture_stderr.string
21
+ $stderr.puts capture_string if capture_string.each_line.count > 1 || !capture_string.include?("conflicting chdir")
22
+ end
23
+ end
24
+
11
25
  def call(env)
12
26
  line = @line.sub('cd', '').strip
13
27
  puts "running $ cd #{line}"
14
- Dir.chdir(line)
15
- nil
28
+
29
+ supress_chdir_warning do
30
+ Dir.chdir(line)
31
+ end
32
+
33
+ return nil
16
34
  end
17
35
  end
18
36
  end
@@ -1,6 +1,10 @@
1
1
  module Rundoc
2
2
  class CodeCommand
3
3
  class NoSuchCommand < Rundoc::CodeCommand
4
+
5
+ def call(env = {})
6
+ raise "No such command registered with rundoc: #{@keyword.inspect} for '#{@keyword} #{@original_args}'"
7
+ end
4
8
  end
5
9
  end
6
10
  end
@@ -0,0 +1,37 @@
1
+ class ::Rundoc::CodeCommand
2
+ class RundocCommand
3
+ class DependOn < ::Rundoc::CodeCommand
4
+
5
+ # Pass in the relative path of another rundoc document in order to
6
+ # run all of it's commands (but not to )
7
+ def initialize(path)
8
+ raise "Path must be relative (i.e. start with `.` or `..`. #{path.inspect} does not" unless path.start_with?(".")
9
+ @path = Pathname.new(path)
10
+ end
11
+
12
+ def to_md(env = {})
13
+ ""
14
+ end
15
+
16
+ def call(env = {})
17
+ current_path = Pathname.new(env[:document_path]).dirname
18
+ document_path = @path.expand_path(current_path)
19
+ # Run the commands, but do not
20
+ puts "rundoc.depend_on: Start executing #{@path.to_s.inspect}"
21
+ output = Rundoc::Parser.new(document_path.read, document_path: document_path.to_s).to_md
22
+ puts "rundoc.depend_on: Done executing #{@path.to_s.inspect}, discarding intermediate document"
23
+ output
24
+ end
25
+
26
+ def hidden?
27
+ true
28
+ end
29
+
30
+ def not_hidden?
31
+ !hidden?
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ Rundoc.register_code_command(:"rundoc.depend_on", ::Rundoc::CodeCommand::RundocCommand::DependOn)
@@ -0,0 +1,41 @@
1
+ class ::Rundoc::CodeCommand
2
+ class RundocCommand
3
+ class Require < ::Rundoc::CodeCommand
4
+
5
+ # Pass in the relative path of another rundoc document in order to
6
+ # run all of it's commands. Resulting contents will be displayed
7
+ # in current document
8
+ def initialize(path)
9
+ raise "Path must be relative (i.e. start with `.` or `..`. #{path.inspect} does not" unless path.start_with?(".")
10
+ @path = Pathname.new(path)
11
+ end
12
+
13
+ def to_md(env = {})
14
+ ""
15
+ end
16
+
17
+ def call(env = {})
18
+ env[:replace] ||= String.new
19
+ current_path = Pathname.new(env[:document_path]).dirname
20
+ document_path = @path.expand_path(current_path)
21
+
22
+ puts "rundoc.require: Start executing #{@path.to_s.inspect}"
23
+ output = Rundoc::Parser.new(document_path.read, document_path: document_path.to_s).to_md
24
+ puts "rundoc.require: Done executing #{@path.to_s.inspect}, putting contents into document"
25
+
26
+ env[:replace] << output
27
+ return ""
28
+ end
29
+
30
+ def hidden?
31
+ true
32
+ end
33
+
34
+ def not_hidden?
35
+ !hidden?
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ Rundoc.register_code_command(:"rundoc.require", ::Rundoc::CodeCommand::RundocCommand::Require)
@@ -19,5 +19,8 @@ module ::Rundoc
19
19
  end
20
20
  end
21
21
 
22
-
23
22
  Rundoc.register_code_command(:rundoc, RundocCommand)
23
+ Rundoc.register_code_command(:"rundoc.configure", RundocCommand)
24
+
25
+ require 'rundoc/code_command/rundoc/depend_on'
26
+ require 'rundoc/code_command/rundoc/require'
@@ -0,0 +1,7 @@
1
+ class Rundoc::CodeCommand::Website
2
+ end
3
+
4
+ require 'rundoc/code_command/website/driver'
5
+ require 'rundoc/code_command/website/screenshot'
6
+ require 'rundoc/code_command/website/visit'
7
+ require 'rundoc/code_command/website/navigate'
@@ -0,0 +1,111 @@
1
+ require 'capybara'
2
+
3
+ Capybara::Selenium::Driver.load_selenium
4
+
5
+ class Rundoc::CodeCommand::Website
6
+ class Driver
7
+ attr_reader :session
8
+
9
+ def initialize(name: , url: , width: 1024, height: 720, visible: false)
10
+ browser_options = ::Selenium::WebDriver::Chrome::Options.new
11
+ browser_options.args << '--headless' unless visible
12
+ browser_options.args << '--disable-gpu' if Gem.win_platform?
13
+ browser_options.args << '--hide-scrollbars'
14
+ # browser_options.args << "--window-size=#{width},#{height}"
15
+ @width = width
16
+ @height = height
17
+
18
+ @driver = Capybara::Selenium::Driver.new(nil, browser: :chrome, options: browser_options)
19
+ driver_name = "rundoc_driver_#{name}".to_sym
20
+ Capybara.register_driver(driver_name) do |app|
21
+ @driver
22
+ end
23
+
24
+ @session = Capybara::Session.new(driver_name)
25
+ end
26
+
27
+ def visit(url)
28
+ @session.visit(url)
29
+ end
30
+
31
+ def timestamp
32
+ Time.now.utc.strftime("%Y%m%d%H%M%S%L%N")
33
+ end
34
+
35
+ def current_url
36
+ session.current_url
37
+ end
38
+
39
+ def scroll(value = 100)
40
+ session.execute_script "window.scrollBy(0,#{value})"
41
+ end
42
+
43
+ def teardown
44
+ session.reset_session!
45
+ end
46
+
47
+ def self.tasks
48
+ @tasks
49
+ end
50
+
51
+ def safe_eval(code)
52
+ @driver.send(:eval, code)
53
+ rescue => e
54
+ msg = String.new
55
+ msg << "Error running code #{code.inspect} at #{current_url.inspect}\n"
56
+ msg << "saving a screenshot to: `tmp/error.png`"
57
+ puts msg
58
+ session.save_screenshot("tmp/error.png")
59
+ raise e
60
+ end
61
+
62
+ def screenshot(upload: false)
63
+ @driver.resize_window_to(@driver.current_window_handle, @width, @height)
64
+ FileUtils.mkdir_p("tmp/rundoc_screenshots")
65
+ file_name = self.class.next_screenshot_name
66
+ file_path = "tmp/rundoc_screenshots/#{file_name}"
67
+ session.save_screenshot(file_path)
68
+
69
+ return file_path unless upload
70
+
71
+ case upload
72
+ when 's3', 'aws'
73
+ puts "Uploading screenshot to S3"
74
+ require 'aws-sdk-s3'
75
+ ENV.fetch('AWS_ACCESS_KEY_ID')
76
+ s3 = Aws::S3::Resource.new(region: ENV.fetch('AWS_REGION'))
77
+
78
+ key = "#{timestamp}/#{file_name}"
79
+ obj = s3.bucket(ENV.fetch('AWS_BUCKET_NAME')).object(key)
80
+ obj.upload_file(file_path)
81
+
82
+ obj.client.put_object_acl(
83
+ acl: 'public-read' ,
84
+ bucket: ENV.fetch('AWS_BUCKET_NAME'),
85
+ key: key
86
+ )
87
+
88
+ obj.public_url
89
+ else
90
+ raise "Upload #{upload.inspect} is not valid, use 's3' instead"
91
+ end
92
+ end
93
+
94
+ @tasks = {}
95
+ def self.add(name, value)
96
+ raise "Task named #{name.inspect} is already started, choose a different name" if @tasks[name]
97
+ @tasks[name] = value
98
+ end
99
+
100
+ def self.find(name)
101
+ raise "Could not find task with name #{name.inspect}, known task names: #{@tasks.keys.inspect}" unless @tasks[name]
102
+ @tasks[name]
103
+ end
104
+
105
+ def self.next_screenshot_name
106
+ @count ||= 0
107
+ @count += 1
108
+ return "screenshot_#{@count}.png"
109
+ end
110
+ end
111
+ end