-
-
Notifications
You must be signed in to change notification settings - Fork 103
Support pipelines. #299
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Support pipelines. #299
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| require 'gitsh/pipeline_environment' | ||
|
|
||
| module Gitsh | ||
| module Commands | ||
| class Pipeline | ||
| def initialize(left, right) | ||
| @left = left | ||
| @right = right | ||
| end | ||
|
|
||
| def execute(env) | ||
| left_env, right_env = PipelineEnvironment.build_pair(env) | ||
| threads = [start_left_thread(left_env), start_right_thread(right_env)] | ||
| wait_for_threads(threads) | ||
| threads.map(&:value).all? | ||
| end | ||
|
|
||
| private | ||
|
|
||
| attr_reader :left, :right, :threads | ||
|
|
||
| def start_left_thread(env) | ||
| Thread.new { execute_left(env) } | ||
| end | ||
|
|
||
| def start_right_thread(env) | ||
| Thread.new { execute_right(env) } | ||
| end | ||
|
|
||
| def execute_left(left_env) | ||
| left.execute(left_env) | ||
| ensure | ||
| left_env.output_stream.close | ||
| end | ||
|
|
||
| def execute_right(right_env) | ||
| right.execute(right_env) | ||
| ensure | ||
| right_env.input_stream.close | ||
| end | ||
|
|
||
| def wait_for_threads(threads) | ||
| threads.map(&:join) | ||
| rescue Interrupt | ||
| threads.each { |thread| thread.raise(Interrupt) } | ||
| retry | ||
| end | ||
| end | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,7 @@ | |
| require 'gitsh/commands/shell_command' | ||
| require 'gitsh/commands/noop' | ||
| require 'gitsh/commands/tree' | ||
| require 'gitsh/commands/pipeline' | ||
|
|
||
| module Gitsh | ||
| class Parser < RLTK::Parser | ||
|
|
@@ -36,6 +37,7 @@ class Parser < RLTK::Parser | |
| clause('.commands SEMICOLON .commands') { |c1, c2| Commands::Tree::Multi.new(c1, c2) } | ||
| clause('.commands OR .commands') { |c1, c2| Commands::Tree::Or.new(c1, c2) } | ||
| clause('.commands AND .commands') { |c1, c2| Commands::Tree::And.new(c1, c2) } | ||
| clause('.commands PIPE .commands') { |c1, c2| Commands::Pipeline.new(c1, c2) } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Metrics/LineLength: Line is too long. [84/80] |
||
| end | ||
|
|
||
| production(:command, 'word argument_list?') do |word, args| | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| require 'delegate' | ||
|
|
||
| module Gitsh | ||
| class PipelineEnvironment < SimpleDelegator | ||
| def self.build_pair(env) | ||
| pipe_reader, pipe_writer = IO.pipe | ||
| [ | ||
| new(env, output_stream: pipe_writer), | ||
| new(env, input_stream: pipe_reader), | ||
| ] | ||
| end | ||
|
|
||
| def initialize(env, options) | ||
| super(env) | ||
| @input_stream = options[:input_stream] | ||
| @output_stream = options[:output_stream] | ||
| end | ||
|
|
||
| def input_stream | ||
| @input_stream || super | ||
| end | ||
|
|
||
| def output_stream | ||
| @output_stream || super | ||
| end | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| require 'spec_helper' | ||
|
|
||
| describe 'Pipeline' do | ||
| it 'passes output of first command to second command' do | ||
| GitshRunner.interactive do |gitsh| | ||
| gitsh.type('init') | ||
| gitsh.type('commit --allow-empty --message "Empty commit"') | ||
| gitsh.type('log --oneline | !wc -l') | ||
|
|
||
| expect(gitsh).to output_no_errors | ||
| expect(gitsh).to output /\b1\b/ | ||
| end | ||
| end | ||
|
|
||
| it 'runs processes in parallel' do | ||
| GitshRunner.interactive do |gitsh| | ||
| gitsh.type_without_waiting('!yes hello | !sed -e "s/ello/i/"') | ||
| gitsh.wait_for_output | ||
| gitsh.send_sigint | ||
|
|
||
| expect(gitsh).to output_no_errors | ||
| expect(gitsh).to output /hi\nhi\n/ | ||
| end | ||
| end | ||
|
|
||
| it 'considers the pipeline to have failed if either command fails' do | ||
| GitshRunner.interactive do |gitsh| | ||
| gitsh.type(':echo $unset | !wc && :echo Success') | ||
|
|
||
| expect(gitsh).to output_error /unset/ | ||
| expect(gitsh).not_to output /Success/ | ||
| end | ||
| end | ||
|
|
||
| it 'supports multi-stage pipelines' do | ||
| GitshRunner.interactive do |gitsh| | ||
| gitsh.type('init') | ||
| gitsh.type('commit --allow-empty -m First --author "A <a@example.com>"') | ||
| gitsh.type('commit --allow-empty -m Second --author "B <b@example.com>"') | ||
| gitsh.type('commit --allow-empty -m Third --author "A <a@example.com>"') | ||
| gitsh.type('commit --allow-empty -m Fourth --author "C <c@example.com>"') | ||
| gitsh.type('log --format="%aN" | !sort -u | !wc -l') | ||
|
|
||
| expect(gitsh).to output_no_errors | ||
| expect(gitsh).to output /\b3\b/ | ||
| end | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| require 'spec_helper' | ||
| require 'gitsh/commands/pipeline' | ||
| require 'gitsh/commands/git_command' | ||
| require 'gitsh/commands/shell_command' | ||
|
|
||
| describe Gitsh::Commands::Pipeline do | ||
| describe '#execute' do | ||
| it 'pipes output of left command to right command' do | ||
| left_command = create_command_double { 'string' } | ||
| right_command = create_command_double { |input| input.upcase } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Style/SymbolProc: Pass &:upcase as an argument to create_command_double instead of a block. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Style/SymbolProc: Pass &:upcase as an argument to create_command_double instead of a block. |
||
| env = build_env | ||
| pipeline = described_class.new(left_command, right_command) | ||
|
|
||
| result = pipeline.execute(env) | ||
|
|
||
| expect(result).to be true | ||
| expect(env.output_stream.string).to eq "STRING\n" | ||
| end | ||
|
|
||
| context 'when the left command fails' do | ||
| it 'returns false' do | ||
| left_command = create_command_double(false) { '' } | ||
| right_command = create_command_double { |input| input.upcase } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Style/SymbolProc: Pass &:upcase as an argument to create_command_double instead of a block. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Style/SymbolProc: Pass &:upcase as an argument to create_command_double instead of a block. |
||
| pipeline = described_class.new(left_command, right_command) | ||
|
|
||
| result = pipeline.execute(build_env) | ||
|
|
||
| expect(result).to be false | ||
| end | ||
| end | ||
|
|
||
| context 'when the right command fails' do | ||
| it 'returns false' do | ||
| left_command = create_command_double { 'string' } | ||
| right_command = create_command_double(false) { '' } | ||
| pipeline = described_class.new(left_command, right_command) | ||
|
|
||
| result = pipeline.execute(build_env) | ||
|
|
||
| expect(result).to be false | ||
| end | ||
| end | ||
| end | ||
|
|
||
| def create_command_double(value=true) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Layout/SpaceAroundEqualsInParameterDefault: Surrounding space missing in default value assignment. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Layout/SpaceAroundEqualsInParameterDefault: Surrounding space missing in default value assignment. |
||
| command = instance_double(Gitsh::Commands::GitCommand) | ||
| allow(command).to receive(:execute) do |env| | ||
| input = env.input_stream.read | ||
| env.output_stream.puts yield(input) | ||
| value | ||
| end | ||
| command | ||
| end | ||
|
|
||
| def build_env | ||
| Gitsh::Environment.new( | ||
| input_stream: instance_double(IO, read: ""), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols. |
||
| output_stream: StringIO.new | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Style/TrailingCommaInArguments: Put a comma after the last parameter of a multiline method call. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Style/TrailingCommaInArguments: Put a comma after the last parameter of a multiline method call. |
||
| ) | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -169,6 +169,14 @@ | |
| expect(result).to be_a(Gitsh::Commands::Tree::Multi) | ||
| end | ||
|
|
||
| it 'parses two commands combined with |' do | ||
| result = parse(tokens( | ||
| [:WORD, 'log'], [:PIPE], [:WORD, '!wc'], [:EOS], | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Layout/FirstParameterIndentation: Indent the first parameter one step more than tokens(. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Layout/FirstParameterIndentation: Indent the first parameter one step more than tokens(. |
||
| )) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Style/TrailingCommaInArguments: Put a comma after the last parameter of a multiline method call. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Style/TrailingCommaInArguments: Put a comma after the last parameter of a multiline method call. |
||
|
|
||
| expect(result).to be_a(Gitsh::Commands::Pipeline) | ||
| end | ||
|
|
||
| it 'parses two commands combined with newlines' do | ||
| result = parse(tokens( | ||
| [:WORD, 'add'], [:SPACE], [:WORD, '.'], | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Metrics/LineLength: Line is too long. [84/80]