roast-ai 0.4.8 → 0.4.9
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/.gitignore +1 -0
- data/.rubocop.yml +1 -0
- data/Gemfile.lock +3 -3
- data/README.md +9 -5
- data/dsl/less_simple.rb +112 -0
- data/dsl/prototype.rb +17 -0
- data/dsl/simple.rb +5 -7
- data/dsl/step_communication.rb +18 -0
- data/examples/grading/README.md +46 -0
- data/examples/grading/analyze_coverage/prompt.md +52 -0
- data/examples/grading/calculate_final_grade.rb +64 -0
- data/examples/grading/format_result.rb +61 -0
- data/examples/grading/generate_grades/prompt.md +105 -0
- data/examples/grading/generate_recommendations/output.txt +17 -0
- data/examples/grading/generate_recommendations/prompt.md +60 -0
- data/examples/grading/read_dependencies/prompt.md +15 -0
- data/examples/grading/verify_mocks_and_stubs/prompt.md +12 -0
- data/examples/grading/verify_test_helpers/prompt.md +53 -0
- data/examples/grading/workflow.md +5 -0
- data/examples/grading/workflow.yml +28 -0
- data/lib/roast/dsl/cog/config.rb +31 -0
- data/lib/roast/dsl/cog/stack.rb +21 -0
- data/lib/roast/dsl/cog/store.rb +26 -0
- data/lib/roast/dsl/cog.rb +70 -0
- data/lib/roast/dsl/cog_execution_context.rb +29 -0
- data/lib/roast/dsl/cogs/cmd.rb +55 -0
- data/lib/roast/dsl/cogs/graph.rb +53 -0
- data/lib/roast/dsl/cogs.rb +65 -0
- data/lib/roast/dsl/config_context.rb +54 -0
- data/lib/roast/dsl/executor.rb +62 -7
- data/lib/roast/dsl/workflow_execution_context.rb +47 -0
- data/lib/roast/error.rb +7 -0
- data/lib/roast/errors.rb +3 -3
- data/lib/roast/graph/edge.rb +25 -0
- data/lib/roast/graph/node.rb +40 -0
- data/lib/roast/graph/quantum_edge.rb +27 -0
- data/lib/roast/graph/threaded_exec.rb +93 -0
- data/lib/roast/graph.rb +233 -0
- data/lib/roast/resources/api_resource.rb +2 -2
- data/lib/roast/resources/url_resource.rb +2 -2
- data/lib/roast/tools/apply_diff.rb +1 -1
- data/lib/roast/tools/ask_user.rb +1 -1
- data/lib/roast/tools/bash.rb +1 -1
- data/lib/roast/tools/cmd.rb +2 -2
- data/lib/roast/tools/coding_agent.rb +2 -2
- data/lib/roast/tools/grep.rb +1 -1
- data/lib/roast/tools/read_file.rb +1 -1
- data/lib/roast/tools/search_file.rb +1 -1
- data/lib/roast/tools/swarm.rb +1 -1
- data/lib/roast/tools/update_files.rb +2 -2
- data/lib/roast/tools/write_file.rb +1 -1
- data/lib/roast/tools.rb +1 -1
- data/lib/roast/value_objects/api_token.rb +1 -1
- data/lib/roast/value_objects/uri_base.rb +1 -1
- data/lib/roast/value_objects/workflow_path.rb +1 -1
- data/lib/roast/version.rb +1 -1
- data/lib/roast/workflow/base_workflow.rb +38 -2
- data/lib/roast/workflow/command_executor.rb +1 -1
- data/lib/roast/workflow/configuration_loader.rb +1 -1
- data/lib/roast/workflow/error_handler.rb +1 -1
- data/lib/roast/workflow/step_executor_registry.rb +1 -1
- data/lib/roast/workflow/step_loader.rb +1 -1
- data/lib/roast/workflow/workflow_executor.rb +1 -1
- data/lib/roast.rb +1 -1
- data/sorbet/config +2 -0
- data/sorbet/rbi/annotations/.gitattributes +1 -0
- data/sorbet/rbi/annotations/activesupport.rbi +495 -0
- data/sorbet/rbi/annotations/faraday.rbi +17 -0
- data/sorbet/rbi/annotations/minitest.rbi +119 -0
- data/sorbet/rbi/annotations/mocha.rbi +34 -0
- data/sorbet/rbi/annotations/rainbow.rbi +269 -0
- data/sorbet/rbi/annotations/webmock.rbi +9 -0
- data/sorbet/rbi/gems/rbs-inline@0.12.0.rbi +2170 -0
- data/sorbet/rbi/gems/{rexml@3.4.1.rbi → rexml@3.4.2.rbi} +284 -239
- data/sorbet/rbi/shims/lib/roast/dsl/config_context.rbi +11 -0
- data/sorbet/rbi/shims/lib/roast/dsl/workflow_execution_context.rbi +11 -0
- data/sorbet/rbi/todo.rbi +7 -0
- metadata +46 -5
- data/package-lock.json +0 -6
- /data/sorbet/rbi/gems/{rack@2.2.17.rbi → rack@2.2.18.rbi} +0 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
Use the provided functions to find and read important dependencies of the provided test file named <%= workflow.file %>.
|
2
|
+
|
3
|
+
The first dependency you should always look for is the source file for the prime subject of the test (whatever class this test file is claiming to test). Use `read_file` to read the subject's source code into your conversation transcript, but only if it's not already there from a previous chat.
|
4
|
+
|
5
|
+
If you can identify other important application-level dependencies then read them too.
|
6
|
+
How many extra dependencies to research is left to your discretion, but ALWAYS make sure you have the subject under test (SUT) in your context before responding.
|
7
|
+
|
8
|
+
Once you are finished using tool functions, respond with the relative path to the source file of the SUT inside <sut> tags. IMPORTANT: Include the full relative path from the project root, including any directory prefixes like lib/, app/, etc.
|
9
|
+
|
10
|
+
Example:
|
11
|
+
|
12
|
+
If you are told to find the dependencies of `test/services/country_db_interface_test.rb`,
|
13
|
+
then you would use the functions as explained above and ultimately respond with `<sut>app/services/country_db_interface.rb</sut>`
|
14
|
+
|
15
|
+
If the file is found at `lib/roast/workflow/workflow_initializer.rb`, respond with `<sut>lib/roast/workflow/workflow_initializer.rb</sut>` (include the lib/ prefix)
|
@@ -0,0 +1,12 @@
|
|
1
|
+
Find places in the provided test code where stubbing and mocking are used. Search for the corresponding implementation source code of those dependencies elsewhere in the codebase to validate that the stub or mock matches the implementation that it is doubling. Use the tool functions provided to find and read the dependencies.
|
2
|
+
|
3
|
+
Once you've found the dependencies, verify that any mocks and stubs accurately reflect the real implementation. If there are discrepancies, list them out alphabetically with:
|
4
|
+
|
5
|
+
1. The name of the mocked/stubbed method
|
6
|
+
2. What the mock/stub expects in terms of arguments and/or return values
|
7
|
+
3. What the actual implementation actually takes as arguments and returns
|
8
|
+
4. Suggestions for fixing the discrepancy
|
9
|
+
|
10
|
+
Note: If there are no discrepancies, do not summarize those that accurately reflect their real implementations in the codebase, just respond "All mocks and stubs verified."
|
11
|
+
|
12
|
+
IMPORTANT: There's absolutely no need for you to waste time grepping for methods/functions that you know belong to testing libraries such as Mocha's `expects` and `stubs`. Only search for the implementation of things that are stubbed and/or mocked in the test to verify whether the test code matches the implementation code.
|
@@ -0,0 +1,53 @@
|
|
1
|
+
Now identify custom test helpers used in this test for the following purpose:
|
2
|
+
|
3
|
+
1. Analyzing if they are used correctly
|
4
|
+
2. Understanding test code that has had significant chunks of implementation abstracted away into helpers
|
5
|
+
3. Fully understanding custom assertions that are not included by default in Ruby on Rails or part of your base knowledge
|
6
|
+
|
7
|
+
Your grep tool function is vital for this work. It provides 4 lines of context before and after the matching line.
|
8
|
+
|
9
|
+
For example, if you call `grep(string: "def assert_sql")`, the output will include:
|
10
|
+
|
11
|
+
```
|
12
|
+
.test/support/helpers/sql_assertions.rb-101- end
|
13
|
+
.test/support/helpers/sql_assertions.rb-102- result
|
14
|
+
.test/support/helpers/sql_assertions.rb-103- end
|
15
|
+
.test/support/helpers/sql_assertions.rb-104-
|
16
|
+
.test/support/helpers/sql_assertions.rb:105: def assert_sql(*patterns_to_match, **kwargs, &block)
|
17
|
+
.test/support/helpers/sql_assertions.rb-106- mysql_only_test!
|
18
|
+
.test/support/helpers/sql_assertions.rb-107-
|
19
|
+
.test/support/helpers/sql_assertions.rb-108- result = T.let(nil, T.nilable(T::Boolean))
|
20
|
+
.test/support/helpers/sql_assertions.rb-109- counter = ActiveRecord::SQLCounter.new(**kwargs)
|
21
|
+
```
|
22
|
+
|
23
|
+
Unfortunately, many test helper methods are undocumented. In those cases (like the example above) the pre-context will be junk. However, there are a number of helper methods that do have very specific and narrow use cases, and those do tend to be well-documented. In those cases, you should use `read_file` to be able to read the full documentation.
|
24
|
+
|
25
|
+
For example, here is the result of calling `grep(string: "def assert_sql_events")`
|
26
|
+
|
27
|
+
```
|
28
|
+
.test/support/helpers/externals_helper.rb-93- # @example Logs events in the list that did not occur
|
29
|
+
.test/support/helpers/externals_helper.rb-94- # expected_queries = { "Shop Load" => 1, "User Load" => 1 }
|
30
|
+
.test/support/helpers/externals_helper.rb-95- # # Fails and reports that User Load occured 0 times instead of expected 1
|
31
|
+
.test/support/helpers/externals_helper.rb-96- # assert_sql_events(expected_queries) { Shop.current_or_find(shop.id) }
|
32
|
+
.test/support/helpers/externals_helper.rb:97: def assert_sql_events(expected_events, &block)
|
33
|
+
.test/support/helpers/externals_helper.rb-98- mysql_only_test!
|
34
|
+
.test/support/helpers/externals_helper.rb-99-
|
35
|
+
.test/support/helpers/externals_helper.rb-100- mysql_events = ExternalsCollector.new(&block).events
|
36
|
+
.test/support/helpers/externals_helper.rb-101- .select { |e| e.first == :mysql }
|
37
|
+
```
|
38
|
+
|
39
|
+
Notice that the documentation for the `assert_sql_events` method is cutoff. Use your `read_file` tool function to get the whole test helper source code and gain better understanding of how it is intended to be used, with the side benefit of also being able to see how it is implemented.
|
40
|
+
|
41
|
+
Note: You will undoubtedly already be familiar with some of Minitest and RSpec's built-in helpers. There is no need to search for those, since they are packaged as gems you won't find them anyway.
|
42
|
+
|
43
|
+
DO NOT FORGET TO PREPEND `def` TO YOUR QUERY TO FIND A METHOD DEFINITION INSTEAD OF USAGES, otherwise you may bring back a very large and useless result set!!!
|
44
|
+
|
45
|
+
Once you are done understanding the custom test helpers used in the test file, analyze and report on whether it seems like any of the helpers are:
|
46
|
+
|
47
|
+
1. Used incorrectly
|
48
|
+
2. Used unnecessarily
|
49
|
+
3. Any other problem related to the use of helper methods
|
50
|
+
|
51
|
+
Where possible, use your best judgment to make recommendations for how to fix problems that you find, but ONLY related to test helpers.
|
52
|
+
|
53
|
+
Note: You are only being used to help find problems so it is not necessary to report on correct usage of helpers or to make positive comments.
|
@@ -0,0 +1,5 @@
|
|
1
|
+
As a senior software engineer and testing expert, evaluate the quality of this test file based on guidelines that will be subsequently provided.
|
2
|
+
|
3
|
+
Next I will now provide the source code of the test that we will be analyzing, and then step you through a series of analysis activities, before finally asking you to provided a final report.
|
4
|
+
|
5
|
+
# <%= file %> <%= File.read(file) %>
|
@@ -0,0 +1,28 @@
|
|
1
|
+
name: Test Grading
|
2
|
+
|
3
|
+
tools:
|
4
|
+
- Roast::Tools::Grep
|
5
|
+
- Roast::Tools::ReadFile
|
6
|
+
- Roast::Tools::SearchFile
|
7
|
+
|
8
|
+
steps:
|
9
|
+
- read_dependencies
|
10
|
+
-
|
11
|
+
- verify_test_helpers
|
12
|
+
- verify_mocks_and_stubs
|
13
|
+
- generate_grades
|
14
|
+
- calculate_final_grade
|
15
|
+
- format_result
|
16
|
+
- generate_recommendations
|
17
|
+
|
18
|
+
analyze_coverage:
|
19
|
+
json: true
|
20
|
+
|
21
|
+
generate_grades:
|
22
|
+
model: o3
|
23
|
+
json: true
|
24
|
+
|
25
|
+
generate_recommendations:
|
26
|
+
model: o3
|
27
|
+
json: true
|
28
|
+
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# typed: false
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Roast
|
5
|
+
module DSL
|
6
|
+
class Cog
|
7
|
+
class Config
|
8
|
+
attr_reader :values
|
9
|
+
|
10
|
+
def initialize(initial = {})
|
11
|
+
@values = initial
|
12
|
+
end
|
13
|
+
|
14
|
+
def merge(config_object)
|
15
|
+
self.class.new(values.merge(config_object.values))
|
16
|
+
end
|
17
|
+
|
18
|
+
# It is recommended to implement a custom config object for a nicer interface,
|
19
|
+
# but for simple cases where it would just be a key value store we provide one by default.
|
20
|
+
|
21
|
+
def []=(key, value)
|
22
|
+
@values[key] = value
|
23
|
+
end
|
24
|
+
|
25
|
+
def [](key)
|
26
|
+
@values[key]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Roast
|
5
|
+
module DSL
|
6
|
+
class Cog
|
7
|
+
class Stack
|
8
|
+
delegate :map, :push, :size, :empty?, to: :@queue
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@queue = []
|
12
|
+
end
|
13
|
+
|
14
|
+
#: () -> Roast::DSL::Cog?
|
15
|
+
def pop
|
16
|
+
@queue.shift
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Roast
|
5
|
+
module DSL
|
6
|
+
class Cog
|
7
|
+
class Store
|
8
|
+
class CogAlreadyDefinedError < Roast::Error; end
|
9
|
+
|
10
|
+
delegate :[], to: :store
|
11
|
+
|
12
|
+
#: (Symbol, Roast::DSL::Cog) -> Roast::DSL::Cog
|
13
|
+
def insert(id, inst)
|
14
|
+
raise CogAlreadyDefinedError if store.key?(id)
|
15
|
+
|
16
|
+
store[id] = inst
|
17
|
+
end
|
18
|
+
|
19
|
+
#: () -> Hash[Symbol, Roast::DSL::Cog]
|
20
|
+
def store
|
21
|
+
@store ||= {}
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Roast
|
5
|
+
module DSL
|
6
|
+
class Cog
|
7
|
+
class CogAlreadyRanError < StandardError; end
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def on_create
|
11
|
+
eigen = self
|
12
|
+
proc do |instance_name = Random.uuid, &action|
|
13
|
+
#: self as Roast::DSL::WorkflowExecutionContext
|
14
|
+
add_cog_instance(instance_name, eigen.new(instance_name, action))
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def on_config
|
19
|
+
eigen = self
|
20
|
+
proc do |cog_name = nil, &configuration|
|
21
|
+
#: self as Roast::DSL::ConfigContext
|
22
|
+
config_object = if cog_name.nil?
|
23
|
+
fetch_execution_scope(eigen)
|
24
|
+
else
|
25
|
+
fetch_or_create_cog_config(eigen, cog_name)
|
26
|
+
end
|
27
|
+
|
28
|
+
config_object.instance_exec(&configuration) if configuration
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def config_class
|
33
|
+
@config_class ||= find_child_config_or_default
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def find_child_config_or_default
|
39
|
+
config_constant = "#{name}::Config"
|
40
|
+
const_defined?(config_constant) ? const_get(config_constant) : Cog::Config # rubocop:disable Sorbet/ConstantsFromStrings
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
attr_reader :name, :output
|
45
|
+
|
46
|
+
def initialize(name, cog_input_proc)
|
47
|
+
@name = name
|
48
|
+
@cog_input_proc = cog_input_proc
|
49
|
+
@finished = false
|
50
|
+
end
|
51
|
+
|
52
|
+
def run!(config, cog_execution_context)
|
53
|
+
raise CogAlreadyRanError if ran?
|
54
|
+
|
55
|
+
@config = config
|
56
|
+
@output = execute(cog_execution_context.instance_exec(&@cog_input_proc))
|
57
|
+
@finished = true
|
58
|
+
end
|
59
|
+
|
60
|
+
def ran?
|
61
|
+
@finished
|
62
|
+
end
|
63
|
+
|
64
|
+
# Inheriting cog must implement this
|
65
|
+
def execute(input)
|
66
|
+
raise NotImplementedError
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Roast
|
5
|
+
module DSL
|
6
|
+
# Contains the cogs already executed in this run.
|
7
|
+
class CogExecutionContext
|
8
|
+
# Raises if you access a cog in an execution block that hasn't already been run.
|
9
|
+
class IncompleteCogExecutionAccessError < StandardError; end
|
10
|
+
|
11
|
+
def initialize(cogs, bound_names)
|
12
|
+
@cogs = cogs
|
13
|
+
bind_cog_methods(bound_names)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def bind_cog_methods(bound_names)
|
19
|
+
bound_names.map do |name|
|
20
|
+
define_singleton_method(name.to_sym, ->(name) do
|
21
|
+
@cogs[name].tap do |cog|
|
22
|
+
raise IncompleteCogExecutionAccessError unless cog.ran?
|
23
|
+
end.output
|
24
|
+
end)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Roast
|
5
|
+
module DSL
|
6
|
+
module Cogs
|
7
|
+
class Cmd < Cog
|
8
|
+
class Output
|
9
|
+
#: String?
|
10
|
+
attr_reader :command_output
|
11
|
+
|
12
|
+
#: String?
|
13
|
+
attr_reader :err
|
14
|
+
|
15
|
+
#: Process::Status?
|
16
|
+
attr_reader :status
|
17
|
+
|
18
|
+
#: (
|
19
|
+
#| String? output,
|
20
|
+
#| String? error,
|
21
|
+
#| Process::Status? status
|
22
|
+
#| ) -> void
|
23
|
+
def initialize(output, error, status)
|
24
|
+
@command_output = output
|
25
|
+
@err = error
|
26
|
+
@status = status
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class Config < Cog::Config
|
31
|
+
#: () -> void
|
32
|
+
def print_all!
|
33
|
+
@values[:print_all] = true
|
34
|
+
end
|
35
|
+
|
36
|
+
#: () -> bool
|
37
|
+
def print_all?
|
38
|
+
!!@values[:print_all]
|
39
|
+
end
|
40
|
+
|
41
|
+
def display!
|
42
|
+
print_all!
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
#: (String) -> Output
|
47
|
+
def execute(input)
|
48
|
+
result = Output.new(*Roast::Helpers::CmdRunner.capture3(input))
|
49
|
+
puts result.command_output if @config.print_all?
|
50
|
+
result
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Roast
|
5
|
+
module DSL
|
6
|
+
module Cogs
|
7
|
+
class Graph < Roast::DSL::Cog
|
8
|
+
#: Proc
|
9
|
+
attr_reader :block
|
10
|
+
|
11
|
+
#: (Symbol) { (Roast::DSL::Cogs::Graph) -> void } -> void
|
12
|
+
def initialize(name, &block)
|
13
|
+
@name = name
|
14
|
+
@block = block
|
15
|
+
@graph = Roast::Graph.new
|
16
|
+
super(name, proc {})
|
17
|
+
end
|
18
|
+
|
19
|
+
#: () -> void
|
20
|
+
def on_invoke
|
21
|
+
populate!(@graph)
|
22
|
+
end
|
23
|
+
|
24
|
+
#: () -> Symbol
|
25
|
+
def store_id
|
26
|
+
@name.to_sym
|
27
|
+
end
|
28
|
+
|
29
|
+
#: (Roast::DSL::Cog) -> void
|
30
|
+
def update(other)
|
31
|
+
return unless other.is_a?(Roast::DSL::Cogs::Graph)
|
32
|
+
|
33
|
+
return if other.block.nil?
|
34
|
+
|
35
|
+
other.populate!(@graph)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Populates the provided graph in-place, with the definition of how to populate in the block
|
39
|
+
#: (Roast::DSL::Cogs::Graph) -> void
|
40
|
+
def populate!(graph)
|
41
|
+
return if @block.nil?
|
42
|
+
|
43
|
+
@block.call(graph)
|
44
|
+
end
|
45
|
+
|
46
|
+
#: () -> void
|
47
|
+
def execute
|
48
|
+
@graph.execute
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# typed: false
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Roast
|
5
|
+
module DSL
|
6
|
+
module Cogs
|
7
|
+
class << self
|
8
|
+
#: (String) -> void
|
9
|
+
def load_all_for(rube_rb_fpath)
|
10
|
+
rube_rb_fpath = File.expand_path(rube_rb_fpath)
|
11
|
+
all_cog_files(rube_rb_fpath).each do |cog_fpath|
|
12
|
+
require cog_fpath
|
13
|
+
end
|
14
|
+
bind_all_cog_invokers
|
15
|
+
end
|
16
|
+
|
17
|
+
#: () -> Array[Class]
|
18
|
+
def all_cog_classes
|
19
|
+
# rubocop:disable Sorbet/ConstantsFromStrings
|
20
|
+
Roast::DSL::Cogs.constants.map do |cog_class_name|
|
21
|
+
Roast::DSL::Cogs.const_get(cog_class_name)
|
22
|
+
end
|
23
|
+
# rubocop:enable Sorbet/ConstantsFromStrings
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
#: () -> void
|
29
|
+
def bind_all_cog_invokers
|
30
|
+
all_cog_classes.each do |cog_class|
|
31
|
+
# At some point we may want to tuck this all into a Roast::DSL::Binding/Scope/Context to avoid polluting toplevel.
|
32
|
+
TOPLEVEL_BINDING.eval(binding_string_for(cog_class))
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
#: (Class) -> String
|
37
|
+
def binding_string_for(cog_class)
|
38
|
+
<<~RUBY
|
39
|
+
def #{cog_class.method_name}(*args, **kwargs, &block)
|
40
|
+
#{cog_class.name}.invoke(*args, **kwargs, &block)
|
41
|
+
end
|
42
|
+
RUBY
|
43
|
+
end
|
44
|
+
|
45
|
+
#: (String) -> Array[String]
|
46
|
+
def all_cog_files(rube_rb_fpath)
|
47
|
+
dirs = [project_cogs_dir(rube_rb_fpath), internal_cogs_dir]
|
48
|
+
dirs.map do |dir|
|
49
|
+
Dir.glob(File.join(dir, "*.rb")) # Just toplevel .rb files
|
50
|
+
end.flatten
|
51
|
+
end
|
52
|
+
|
53
|
+
#: (String) -> String
|
54
|
+
def project_cogs_dir(rube_rb_fpath)
|
55
|
+
File.join(File.dirname(rube_rb_fpath), "cogs")
|
56
|
+
end
|
57
|
+
|
58
|
+
#: () -> String
|
59
|
+
def internal_cogs_dir
|
60
|
+
File.join(File.dirname(__FILE__), "cogs")
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Roast
|
5
|
+
module DSL
|
6
|
+
class ConfigContext
|
7
|
+
def initialize(cogs, config_proc)
|
8
|
+
@cogs = cogs
|
9
|
+
@executor_scoped_configs = {}
|
10
|
+
@cog_scoped_configs = {}
|
11
|
+
@config_proc = config_proc
|
12
|
+
end
|
13
|
+
|
14
|
+
def fetch_merged_config(cog_class, name = nil)
|
15
|
+
# All configs have an entry, even if it's empty.
|
16
|
+
configs = fetch_execution_scope(cog_class)
|
17
|
+
instance_configs = fetch_cog_config(cog_class, name) unless name.nil?
|
18
|
+
configs = configs.merge(instance_configs) if instance_configs
|
19
|
+
configs
|
20
|
+
end
|
21
|
+
|
22
|
+
def prepare!
|
23
|
+
bind_default_cogs
|
24
|
+
instance_eval(&@config_proc)
|
25
|
+
end
|
26
|
+
|
27
|
+
#: () -> void
|
28
|
+
def bind_default_cogs
|
29
|
+
bind_cog(Cogs::Cmd, :cmd)
|
30
|
+
end
|
31
|
+
|
32
|
+
def fetch_cog_config(cog_class, name)
|
33
|
+
@cog_scoped_configs[cog_class][name]
|
34
|
+
end
|
35
|
+
|
36
|
+
def fetch_or_create_cog_config(cog_class, name)
|
37
|
+
@cog_scoped_configs[cog_class][name] = cog_class.config_class.new unless @cog_scoped_configs.key?(name)
|
38
|
+
@cog_scoped_configs[cog_class][name]
|
39
|
+
end
|
40
|
+
|
41
|
+
def fetch_execution_scope(cog_class)
|
42
|
+
@executor_scoped_configs[cog_class]
|
43
|
+
end
|
44
|
+
|
45
|
+
def bind_cog(cog_class, method_name)
|
46
|
+
@cog_scoped_configs[cog_class] = {}
|
47
|
+
@executor_scoped_configs[cog_class] = cog_class.config_class.new
|
48
|
+
instance_eval do
|
49
|
+
define_singleton_method(method_name, &cog_class.on_config)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/roast/dsl/executor.rb
CHANGED
@@ -4,23 +4,78 @@
|
|
4
4
|
module Roast
|
5
5
|
module DSL
|
6
6
|
class Executor
|
7
|
+
class ExecutorError < Roast::Error; end
|
8
|
+
class ExecutorAlreadyPreparedError < ExecutorError; end
|
9
|
+
class ExecutorAlreadyCompletedError < ExecutorError; end
|
10
|
+
|
7
11
|
class << self
|
8
12
|
def from_file(workflow_path)
|
9
|
-
|
13
|
+
run!(File.read(workflow_path))
|
10
14
|
end
|
11
15
|
|
12
16
|
private
|
13
17
|
|
14
|
-
def
|
15
|
-
new
|
18
|
+
def run!(workflow_definition)
|
19
|
+
executor = new
|
20
|
+
executor.prepare!(workflow_definition)
|
21
|
+
executor.start!
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def prepare!(input)
|
26
|
+
# You can only initialize an executor once.
|
27
|
+
raise ExecutorAlreadyPreparedError if @prepared
|
28
|
+
|
29
|
+
extract_dsl_procs(input)
|
30
|
+
@cogs = Cog::Store.new
|
31
|
+
@cog_stack = Cog::Stack.new
|
32
|
+
|
33
|
+
@config_context = ConfigContext.new(@cogs, @config_proc)
|
34
|
+
@config_context.prepare!
|
35
|
+
@execution_context = WorkflowExecutionContext.new(@cogs, @cog_stack, @execution_proc)
|
36
|
+
@execution_context.prepare!
|
37
|
+
|
38
|
+
@prepared = true
|
39
|
+
end
|
40
|
+
|
41
|
+
def start!
|
42
|
+
# Now we run the cogs!
|
43
|
+
# You can only do this once, executors are not reusable to avoid state pollution
|
44
|
+
raise ExecutorAlreadyCompletedError if @completed
|
45
|
+
|
46
|
+
@cog_stack.map do |name, cog|
|
47
|
+
cog.run!(
|
48
|
+
@config_context.fetch_merged_config(cog.class, name.to_sym),
|
49
|
+
@execution_context.cog_execution_context,
|
50
|
+
)
|
16
51
|
end
|
52
|
+
|
53
|
+
@completed = true
|
17
54
|
end
|
18
55
|
|
19
|
-
|
56
|
+
def prepared?
|
57
|
+
@prepared ||= false
|
58
|
+
end
|
59
|
+
|
60
|
+
def completed?
|
61
|
+
@completed ||= false
|
62
|
+
end
|
63
|
+
|
64
|
+
#: { () [self: ConfigContext] -> void} -> void
|
65
|
+
def config(&block)
|
66
|
+
@config_proc = block
|
67
|
+
end
|
68
|
+
|
69
|
+
#: { () [self: WorkflowExecutionContext] -> void} -> void
|
70
|
+
def execute(&block)
|
71
|
+
@execution_proc = block
|
72
|
+
end
|
20
73
|
|
21
|
-
|
22
|
-
|
23
|
-
|
74
|
+
# Separating the instance evals ensures that we can reuse the same cog method
|
75
|
+
# names between config and execute, while have the backing objects be completely
|
76
|
+
# different. This means we have an enforced separation between configuring and running.
|
77
|
+
def extract_dsl_procs(input)
|
78
|
+
instance_eval(input)
|
24
79
|
end
|
25
80
|
end
|
26
81
|
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Roast
|
5
|
+
module DSL
|
6
|
+
class WorkflowExecutionContext
|
7
|
+
def initialize(cogs, cog_stack, execution_proc)
|
8
|
+
@cogs = cogs
|
9
|
+
@cog_stack = cog_stack
|
10
|
+
@execution_proc = execution_proc
|
11
|
+
@bound_names = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def prepare!
|
15
|
+
bind_default_cogs
|
16
|
+
instance_eval(&@execution_proc)
|
17
|
+
end
|
18
|
+
|
19
|
+
def cog_execution_context
|
20
|
+
@cog_execution_context ||= CogExecutionContext.new(@cogs, @bound_names)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def add_cog_instance(name, cog)
|
26
|
+
@cogs.insert(name, cog)
|
27
|
+
@cog_stack.push([name, cog])
|
28
|
+
end
|
29
|
+
|
30
|
+
def output(name)
|
31
|
+
@cogs[name].output
|
32
|
+
end
|
33
|
+
|
34
|
+
#: () -> void
|
35
|
+
def bind_default_cogs
|
36
|
+
bind_cog(Cogs::Cmd, :cmd)
|
37
|
+
end
|
38
|
+
|
39
|
+
def bind_cog(cog_class, name)
|
40
|
+
@bound_names << name
|
41
|
+
instance_eval do
|
42
|
+
define_singleton_method(name, &cog_class.on_create)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|