omoteura-core 0.1.1 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2973f2f5bef98037f08e636f77e98886acbe82496b5db1997673d524ec550ba1
4
- data.tar.gz: fa0ec2892824e5e61ce9b70affddf842ddc016be43c822e50df51c343b7d4ea5
3
+ metadata.gz: e172672c45474fc44869d70f9e0812ff21f47462d83cd93c9961e30c18cee160
4
+ data.tar.gz: 41798125dd13656c98913fa30eb2749ead7a2789db8cdd37b17c062cc0126587
5
5
  SHA512:
6
- metadata.gz: b050f681961086f47a2a5470590fa128a850a80c9eb2cde00f8ce4b0086188a800aae5f2a32c1481d681ac1f98b431a4ff882c2f49bc1a5bb8650b15e69f5c1a
7
- data.tar.gz: cbe4f109d8a72330214e0923356cbc07af84b4246ad27338aa8fed71887e2af79a62347ec5a33f220298b928d640bccf69de9a81d245e20e4f3b6a539119e16a
6
+ metadata.gz: 2b42bd503d9a835faf6570bb70880e384ab7b9321a99df4429cf77286ddf8c980e4645cfaf502e1762f09db7ffe18f4d934a42cd102eed9d33c64baa534fe91d
7
+ data.tar.gz: e00f1732c0972ce1aff13cb897500656b7ff3c6401f2e5449e98e8095ad79ad52ddd52e0b4c5500ce4647bc1e22c1dd585f4167fcac65371beead57868bae4ea
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Omoteura
4
- VERSION = "0.1.1"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -5,6 +5,7 @@ module Omoteura
5
5
  # for the editor's coverage panel. Separate from diagrams: it works on any
6
6
  # file and needs none.
7
7
  #
8
+ # The examples are RSpec's or Minitest's, from the file's spec or test.
8
9
  # An item counts as covered by an example that names it, in the example's
9
10
  # description or its code: "validates :email" by an example mentioning
10
11
  # email, "def display_name" by one inside describe "#display_name". The
@@ -40,16 +41,19 @@ module Omoteura
40
41
  Example = Data.define(:path, :line, :label, :description, :source, :status)
41
42
  private_constant :ONE_LINER, :Example
42
43
 
43
- # The specs for a file by RSpec's conventions: app/models/user.rb is
44
- # tested in spec/models/user_spec.rb, lib/a/b.rb in spec/a/b_spec.rb or
45
- # spec/lib/a/b_spec.rb.
44
+ # The tests for a file by RSpec's and Rails' conventions:
45
+ # app/models/user.rb is tested in spec/models/user_spec.rb or
46
+ # test/models/user_test.rb, lib/a/b.rb in spec/a/b_spec.rb,
47
+ # spec/lib/a/b_spec.rb, test/a/b_test.rb or test/lib/a/b_test.rb. A
48
+ # project with both kinds gets both.
46
49
  def spec_paths(code_path)
47
50
  base = code_path.delete_suffix(File.extname(code_path))
48
- candidates =
49
- if base.start_with?("app/") then ["spec/#{base.delete_prefix("app/")}_spec.rb"]
50
- elsif base.start_with?("lib/") then ["spec/#{base.delete_prefix("lib/")}_spec.rb", "spec/#{base}_spec.rb"]
51
- else ["spec/#{base}_spec.rb"]
51
+ names =
52
+ if base.start_with?("app/") then [base.delete_prefix("app/")]
53
+ elsif base.start_with?("lib/") then [base.delete_prefix("lib/"), base]
54
+ else [base]
52
55
  end
56
+ candidates = names.map { |name| "spec/#{name}_spec.rb" } + names.map { |name| "test/#{name}_test.rb" }
53
57
  candidates.select { |path| (full = @config.expand(path)) && File.file?(full) }
54
58
  end
55
59
 
@@ -66,7 +70,9 @@ module Omoteura
66
70
  (described + undescribed).sort_by(&:start_line).map do |entry|
67
71
  result = recorded[entry.start_line]
68
72
  static = (entry.kind == :spec) ? entry.name : nil
69
- description = [static, result&.full_description].compact.uniq.join("\n")
73
+ # A Minitest test method, such as test_total, names what it tests
74
+ # between underscores.
75
+ description = [static, static&.tr("_", " "), result&.full_description].compact.uniq.join("\n")
70
76
  Example.new(path: spec_path, line: entry.start_line, label: static || result&.full_description || entry.name, description: description,
71
77
  source: lines[(entry.start_line - 1)...entry.end_line].join, status: result&.status)
72
78
  end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "coverage"
4
+ require "json"
5
+ require "time"
6
+
7
+ module Omoteura
8
+ # Builds the test results file that the RSpec formatter and the Minitest
9
+ # reporter write and TestResults reads. Loaded on its own by those, so it
10
+ # requires nothing else of Omoteura.
11
+ module ResultsFile
12
+ FORMAT_VERSION = 1
13
+ DEFAULT_PATH = "tmp/omoteura/results.json"
14
+ ANSI_ESCAPE = /\e\[[0-9;]*m/
15
+ # Code that is not the project's own, or is the tests themselves.
16
+ NOT_PROJECT_CODE = %r{\A(?:spec|test|vendor|tmp|node_modules)/}
17
+
18
+ module_function
19
+
20
+ # examples are Hashes with :id, :file_path, :line_number,
21
+ # :full_description, :status ("passed", "failed" or "pending") and
22
+ # :message.
23
+ def generate(examples)
24
+ counts = examples.map { |e| e[:status] }.tally
25
+ data = {
26
+ version: FORMAT_VERSION,
27
+ generated_at: Time.now.utc.iso8601,
28
+ summary: {examples: examples.size, failures: counts.fetch("failed", 0), pending: counts.fetch("pending", 0)},
29
+ examples: examples
30
+ }
31
+ executed = executed_lines
32
+ data[:executed_lines] = executed if executed
33
+ JSON.pretty_generate(data) + "\n"
34
+ end
35
+
36
+ # A message as a terminal would show it, without the colors.
37
+ def plain(text)
38
+ text&.gsub(ANSI_ESCAPE, "")
39
+ end
40
+
41
+ # Line numbers that ran, by file relative to the working directory, or
42
+ # nil when coverage is not being measured. Something else, such as
43
+ # SimpleCov, must have started it: a coverage tool starting after the
44
+ # test framework's integration would fail.
45
+ def executed_lines
46
+ return nil unless ::Coverage.running?
47
+
48
+ root = File.join(Dir.pwd, "")
49
+ ::Coverage.peek_result.each_with_object({}) do |(path, counts), executed|
50
+ next unless path.start_with?(root) && path.end_with?(".rb")
51
+
52
+ relative = path.delete_prefix(root)
53
+ next if relative.match?(NOT_PROJECT_CODE)
54
+
55
+ # Hash for Coverage.start(lines: true), as SimpleCov uses; an
56
+ # Array of line counts for the older Coverage.start.
57
+ lines = counts.is_a?(Hash) ? counts[:lines] : counts
58
+ next unless lines
59
+
60
+ executed[relative] = lines.each_index.select { |i| lines[i]&.positive? }.map { |i| i + 1 }
61
+ end
62
+ end
63
+ end
64
+ end
@@ -36,7 +36,9 @@ module Omoteura
36
36
  EXAMPLE_GROUPS = %i[describe context feature example_group xdescribe xcontext xfeature fdescribe fcontext ffeature].freeze
37
37
  EXAMPLES = %i[it specify example scenario xit xspecify xexample xscenario fit fspecify fexample fscenario].freeze
38
38
 
39
- Group = Data.define(:description, :module)
39
+ # test_class is true for a Minitest test class, where test "…" blocks
40
+ # are examples.
41
+ Group = Data.define(:description, :module, :test_class)
40
42
 
41
43
  attr_reader :entries
42
44
 
@@ -57,10 +59,29 @@ module Omoteura
57
59
  def visit_def_node(node)
58
60
  name = node.receiver.is_a?(Prism::SelfNode) ? "self.#{node.name}" : node.name.name
59
61
  add(:method, name, node)
62
+ # def test_… in a test class is a test as much as test "…" is. A link
63
+ # by its name finds the example first, at the same line.
64
+ if node.receiver.nil? && @groups.last&.test_class && name.start_with?("test_")
65
+ description = full_description(name)
66
+ add(:spec, description, node) if description
67
+ end
68
+ super
69
+ end
70
+
71
+ # A Minitest test class is an example group described by its name, as
72
+ # "UserTest", so that its tests are examples like RSpec's.
73
+ def visit_class_node(node)
74
+ return super unless test_class?(node)
75
+
76
+ description = full_description(node.constant_path.slice)
77
+ add(:spec, description, node, group: true) if description
78
+ @groups.push(Group.new(description: description, module: true, test_class: true))
60
79
  super
80
+ @groups.pop
61
81
  end
62
82
 
63
83
  def visit_call_node(node)
84
+ return visit_minitest_test(node) if minitest_test?(node)
64
85
  return super unless spec_call?(node)
65
86
 
66
87
  part, is_module = description_of(node)
@@ -68,7 +89,7 @@ module Omoteura
68
89
  add(:spec, description, node, group: EXAMPLE_GROUPS.include?(node.name)) if description
69
90
 
70
91
  if EXAMPLE_GROUPS.include?(node.name)
71
- @groups.push(Group.new(description: description, module: is_module))
92
+ @groups.push(Group.new(description: description, module: is_module, test_class: false))
72
93
  super
73
94
  @groups.pop
74
95
  else
@@ -78,6 +99,33 @@ module Omoteura
78
99
 
79
100
  private
80
101
 
102
+ # test "requires an email" do … end, as ActiveSupport::TestCase defines
103
+ # tests, directly in a test class.
104
+ def minitest_test?(node)
105
+ node.name == :test && node.receiver.nil? && node.block && @groups.last&.test_class &&
106
+ node.arguments&.arguments&.first.is_a?(Prism::StringNode) || false
107
+ end
108
+
109
+ def visit_minitest_test(node)
110
+ description = full_description(node.arguments.arguments.first.unescaped)
111
+ add(:spec, description, node) if description
112
+ # Groups opened inside the test are not examples of the class.
113
+ @groups.push(Group.new(description: nil, module: false, test_class: false))
114
+ visit_child_nodes(node)
115
+ @groups.pop
116
+ end
117
+
118
+ # A class named like UserTest, or one that inherits from a class named
119
+ # like ActiveSupport::TestCase or Minitest::Test. Symbols are compared
120
+ # so that ordinary classes cost no allocation.
121
+ def test_class?(node)
122
+ return true if node.name.end_with?("Test")
123
+
124
+ superclass = node.superclass
125
+ (superclass.is_a?(Prism::ConstantReadNode) || superclass.is_a?(Prism::ConstantPathNode)) &&
126
+ superclass.name.end_with?("Test", "TestCase")
127
+ end
128
+
81
129
  # Cuts the statement's first line out of the source by byte offsets.
82
130
  # Slicing the whole statement would copy most of the file for every
83
131
  # class or describe block, and again for each level of nesting.
@@ -1,14 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
+ require "omoteura/results_file"
4
5
 
5
6
  module Omoteura
6
- # Test results written by Omoteura::RSpec::Formatter.
7
+ # Test results written by Omoteura::RSpec::Formatter or the Minitest
8
+ # reporter; see ResultsFile.
7
9
  class TestResults
8
10
  class Invalid < Error; end
9
11
 
10
- DEFAULT_PATH = "tmp/omoteura/results.json"
11
- SUPPORTED_VERSION = 1
12
+ DEFAULT_PATH = ResultsFile::DEFAULT_PATH
13
+ SUPPORTED_VERSION = ResultsFile::FORMAT_VERSION
12
14
 
13
15
  Example = Data.define(:file_path, :line_number, :full_description, :status, :message)
14
16
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omoteura-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - KAIBYAKU K.K
@@ -35,6 +35,7 @@ files:
35
35
  - lib/omoteura/outline.rb
36
36
  - lib/omoteura/project.rb
37
37
  - lib/omoteura/resolver.rb
38
+ - lib/omoteura/results_file.rb
38
39
  - lib/omoteura/source_index.rb
39
40
  - lib/omoteura/test_results.rb
40
41
  - lib/omoteura/writer.rb