psychic-runner 0.0.2

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 67df9a60685ee7062b1b4a26ef84a74157338023
4
+ data.tar.gz: 5fd8577946b451d79ac3b7b928538089d66f512e
5
+ SHA512:
6
+ metadata.gz: 8ef2abd35ba79d48c86b27389731b6b6962dcd793a4810b144b439d1b846351f7052cc213a4d77463c468f7dc22ba825180e723244e45eab4001026c5b0f8a62
7
+ data.tar.gz: f254488c0fa32cd13e8f8c7477988d3d61b7a5b165e08d31626f07834d0341fd33097ac85eeb101ae73d3fbeb1e7b9d5a8aea864065b620eb8bc02ab6afd9a5e
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ Gemfile.lock
2
+ tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,2 @@
1
+ inherit_from: .rubocop_todo.yml
2
+
data/.rubocop_todo.yml ADDED
@@ -0,0 +1,29 @@
1
+ # This configuration was generated by `rubocop --auto-gen-config`
2
+ # on 2014-11-20 15:08:06 -0500 using RuboCop version 0.27.1.
3
+ # The point is for the user to remove these configuration records
4
+ # one by one as the offenses are removed from the code base.
5
+ # Note that changes in the inspected code, or installation of new
6
+ # versions of RuboCop, may require this file to be generated again.
7
+
8
+ # Offense count: 7
9
+ # Configuration parameters: AllowURI, URISchemes.
10
+ Metrics/LineLength:
11
+ Max: 104
12
+
13
+ # Offense count: 1
14
+ # Configuration parameters: CountComments.
15
+ Metrics/MethodLength:
16
+ Max: 11
17
+
18
+ # Offense count: 14
19
+ Style/Documentation:
20
+ Enabled: false
21
+
22
+ # Offense count: 1
23
+ # Configuration parameters: MinBodyLength.
24
+ Style/GuardClause:
25
+ Enabled: false
26
+
27
+ # Offense count: 2
28
+ Style/RegexpLiteral:
29
+ MaxSlashes: 0
data/.travis.yml ADDED
@@ -0,0 +1,30 @@
1
+ language: php
2
+ php:
3
+ - "5.6"
4
+ - "5.5"
5
+ - "5.4"
6
+ - "5.3"
7
+ - hhvm
8
+
9
+ matrix:
10
+ allow_failures:
11
+ - php: hhvm
12
+
13
+ branches:
14
+ only:
15
+ - master
16
+ - working
17
+
18
+ before_script:
19
+ - composer install --prefer-source
20
+ - vendor/bin/parallel-lint --exclude vendor .
21
+ - vendor/bin/php-cs-fixer fix --dry-run --level psr2 .
22
+
23
+ after_script:
24
+ - php vendor/bin/coveralls -v
25
+
26
+ notifications:
27
+ email:
28
+ - jamie.hannaford@rackspace.com
29
+ - glen.campbell@rackspace.com
30
+ - shaunak.kashyap@rackspace.com
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in psychic-runner.gemspec
4
+ gemspec
5
+ gem 'pry'
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Max Lincoln
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,70 @@
1
+ **php-opencloud**
2
+ =============
3
+ PHP SDK for OpenStack/Rackspace APIs
4
+
5
+ [![Latest Stable Version](https://poser.pugx.org/rackspace/php-opencloud/v/stable.png)](https://packagist.org/packages/rackspace/php-opencloud) [![Travis CI](https://secure.travis-ci.org/rackspace/php-opencloud.png)](https://travis-ci.org/rackspace/php-opencloud) [![Total Downloads](https://poser.pugx.org/rackspace/php-opencloud/downloads.png)](https://packagist.org/packages/rackspace/php-opencloud)
6
+
7
+ For SDKs in different languages, see http://developer.rackspace.com.
8
+
9
+ The PHP SDK should work with most OpenStack-based cloud deployments,
10
+ though it specifically targets the Rackspace public cloud. In
11
+ general, whenever a Rackspace deployment is substantially different
12
+ than a pure OpenStack one, a separate Rackspace subclass is provided
13
+ so that you can still use the SDK with a pure OpenStack instance
14
+ (for example, see the `OpenStack` class (for OpenStack) and the
15
+ `Rackspace` subclass).
16
+
17
+ Requirements
18
+ ------------
19
+ * PHP >=5.3.3
20
+ * cURL extension for PHP
21
+
22
+ Installation
23
+ ------------
24
+ You must install this library through Composer:
25
+
26
+ ```bash
27
+ # Install Composer
28
+ curl -sS https://getcomposer.org/installer | php
29
+
30
+ # Require php-opencloud as a dependency
31
+ php composer.phar require rackspace/php-opencloud
32
+ ```
33
+
34
+ Once you have installed the library, you will need to load Composer's autoloader (which registers all the required
35
+ namespaces):
36
+
37
+ ```php
38
+ require 'vendor/autoload.php';
39
+ ```
40
+
41
+ And you're ready to go!
42
+
43
+ You can also check out the [Getting Started guide](docs/getting-started.md) for a quick tutorial.
44
+
45
+ - - -
46
+
47
+ Alternatively, if you would like to fork or clone the repository into a directory (to work and submit pull requests),
48
+ you will need to execute:
49
+
50
+ ```bash
51
+ php composer.phar install
52
+ ```
53
+
54
+ Instead of the `require` command. You can also specify the `--no-dev` option if you do not want to install phpDocumentor
55
+ (which has lots of vendor folders).
56
+
57
+ Support and Feedback
58
+ --------------------
59
+ Your feedback is appreciated! If you have specific problems or bugs with this SDK, please file an issue on Github. We
60
+ also have a [mailing list](https://groups.google.com/forum/#!forum/php-opencloud), so feel free to join to keep up to
61
+ date with all the latest changes and announcements to the library.
62
+
63
+ For general feedback and support requests, send an email to sdk-support@rackspace.com.
64
+
65
+ You can also find assistance via IRC on #rackspace at freenode.net.
66
+
67
+ Contributing
68
+ ------------
69
+ If you'd like to contribute to the project, or require help running the unit/acceptance tests, please view the
70
+ [contributing guidelines](https://github.com/rackspace/php-opencloud/blob/master/CONTRIBUTING.md).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rubocop/rake_task'
3
+ require 'rake/notes/rake_task'
4
+ require 'rspec/core/rake_task'
5
+
6
+ task default: [:spec, :rubocop, :notes]
7
+
8
+ RSpec::Core::RakeTask.new('spec')
9
+ RuboCop::RakeTask.new(:rubocop) do |task|
10
+ # abort rake on failure
11
+ task.fail_on_error = true
12
+ end
data/bin/psychic ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require 'psychic/cli'
3
+
4
+ Psychic::CLI.start
@@ -0,0 +1,37 @@
1
+ require 'thor'
2
+ require 'psychic/runner'
3
+
4
+ module Psychic
5
+ class CLI < Thor
6
+ desc 'run_task <name>', 'Executes a custom task by name'
7
+ def run_task(task_name, *args)
8
+ result = runner.execute_task(task_name, *args)
9
+ result.error!
10
+ say_status :success, task_name
11
+ rescue Psychic::Shell::ExecutionError => e
12
+ say_status :failed, task_name, :red
13
+ say e.execution_result if e.execution_result
14
+ end
15
+
16
+ desc 'run_sample <name>', 'Executes a code sample'
17
+ def run_sample(sample_name, *args)
18
+ result = runner.run_sample(sample_name, *args)
19
+ result.error!
20
+ say_status :success, sample_name
21
+ rescue Errno::ENOENT => e
22
+ say_status :failed, "No code sample found for #{sample_name}", :red
23
+ rescue Psychic::Shell::ExecutionError => e
24
+ say_status :failed, "Executing sample #{sample_name}", :red
25
+ say e.execution_result if e.execution_result
26
+ end
27
+
28
+ private
29
+
30
+ def runner
31
+ # Psychic::Shell.shell = shell
32
+ @runner ||= Psychic::Runner.new
33
+ end
34
+ end
35
+ end
36
+
37
+ # require 'psychic/commands/exec'
@@ -0,0 +1,19 @@
1
+ module Psychic
2
+ module Commands
3
+ class Exec < Thor
4
+ desc 'task <name>', 'Executes a custom task by name'
5
+ def task(task_name)
6
+ # Psychic::Shell.shell = shell
7
+ runner = Psychic::Runner.new
8
+ result = runner.public_send(task_name.to_sym)
9
+ result.error!
10
+ say_status :success, task_name
11
+ rescue Psychic::Shell::ExecutionError => e
12
+ say_status :failed, task_name, :red
13
+ say e.execution_result if e.execution_result
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ Psychic::CLI.register(Psychic::Commands::Exec, 'exec', 'exec <task>', 'Execute things via psychic')
@@ -0,0 +1,17 @@
1
+ require 'logger'
2
+
3
+ module Psychic
4
+ module Logger
5
+ def logger
6
+ @logger ||= new_logger
7
+ end
8
+
9
+ def new_logger(_io = $stdout, _level = :debug)
10
+ ::Logger.new(STDOUT)
11
+ end
12
+
13
+ def log_level=(level)
14
+ logger.level = level
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,68 @@
1
+ module Psychic
2
+ class Runner
3
+ module BaseRunner
4
+ include Psychic::Shell
5
+ include Psychic::Logger
6
+
7
+ attr_reader :known_tasks
8
+ attr_reader :cwd
9
+
10
+ module ClassMethods
11
+ attr_accessor :magic_file_pattern
12
+
13
+ def register_runner
14
+ Psychic::Runner::ColdRunnerRegistry.register(self)
15
+ end
16
+
17
+ def magic_file(pattern) # rubocop:disable Style/TrivialAccessors
18
+ @magic_file_pattern = pattern
19
+ end
20
+ end
21
+
22
+ def self.included(base)
23
+ base.extend(ClassMethods)
24
+ end
25
+
26
+ def initialize(opts = {})
27
+ opts[:cwd] ||= Dir.pwd
28
+ @logger = opts[:logger] || new_logger
29
+ @cwd = opts[:cwd]
30
+ @opts = opts
31
+ end
32
+
33
+ def respond_to_missing?(task, include_all = false)
34
+ return true if known_tasks.include?(task.to_s)
35
+ super
36
+ end
37
+
38
+ def method_missing(task, *args, &block)
39
+ execute_task(task, *args)
40
+ rescue Psychic::Runner::TaskNotImplementedError
41
+ super
42
+ end
43
+
44
+ # Reserved words
45
+
46
+ def execute(command, *args)
47
+ full_cmd = [command, *args].join(' ')
48
+ logger.info("Executing #{full_cmd}")
49
+ shell.execute(full_cmd, @opts)
50
+ end
51
+
52
+ def command_for_task(task, *_args)
53
+ task_name = task.to_s
54
+ self[task_name]
55
+ end
56
+
57
+ def execute_task(task, *args)
58
+ command = command_for_task(task, *args)
59
+ fail Psychic::Runner::TaskNotImplementedError if command.nil?
60
+ execute(command, *args)
61
+ end
62
+
63
+ def active?
64
+ self.class.magic_file_pattern ? false : Dir["#{@cwd}/#{self.class.magic_file_pattern}"]
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,38 @@
1
+ module Psychic
2
+ class Runner
3
+ module Cold
4
+ class ShellScriptRunner
5
+ include BaseRunner
6
+ EXTENSIONS = ['.sh', '']
7
+ magic_file 'scripts/*'
8
+ register_runner
9
+
10
+ def initialize(opts)
11
+ super
12
+ @known_tasks = Dir["#{@cwd}/scripts/*"].map do | script |
13
+ File.basename(script, File.extname(script)) if EXTENSIONS.include?(File.extname(script))
14
+ end
15
+ end
16
+
17
+ def [](task_name)
18
+ task = task_name.to_s
19
+ script = Dir["#{@cwd}/scripts/#{task}{.sh,}"].first
20
+ if script
21
+ cmd = Psychic::Util.relativize(script, @cwd)
22
+ cmd = [cmd, args_for_task(task_name)].compact.join(' ')
23
+ "./#{cmd}" unless cmd.to_s.start_with? '/'
24
+ end
25
+ end
26
+
27
+ def args_for_task(task)
28
+ # HACK: Need a better way to deal with args
29
+ '{{sample_file}}' if task == 'run_sample'
30
+ end
31
+
32
+ def active?
33
+ true
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,31 @@
1
+ module Psychic
2
+ class Runner
3
+ class ColdRunnerRegistry
4
+ include Psychic::Logger
5
+
6
+ BUILT_IN_DIR = File.expand_path('../cold', __FILE__)
7
+
8
+ class << self
9
+ def autoload_runners!
10
+ # Load built-in runners
11
+ Dir["#{BUILT_IN_DIR}/*.rb"].each do |cold_runner_file|
12
+ require cold_runner_file
13
+ end
14
+ end
15
+
16
+ def runner_classes
17
+ @runner_classes ||= Set.new
18
+ end
19
+
20
+ def register(klass)
21
+ runner_classes.add klass
22
+ end
23
+
24
+ def active_runners(opts)
25
+ runners = runner_classes.map { |k| k.new(opts) }
26
+ runners.select(&:active?)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,25 @@
1
+ module Psychic
2
+ class Runner
3
+ class HotRunner
4
+ include BaseRunner
5
+ def initialize(opts = {})
6
+ hints = opts.delete :hints
7
+ super
8
+ @hints = Psychic::Util.stringified_hash(hints || load_hints || {})
9
+ @tasks = @hints['tasks'] || {}
10
+ @known_tasks = @tasks.keys
11
+ end
12
+
13
+ def [](task_name)
14
+ @tasks[task_name]
15
+ end
16
+
17
+ private
18
+
19
+ def load_hints
20
+ hints_file = Dir["#{@cwd}/psychic-hints.{yaml,yml}"].first
21
+ YAML.load(File.read(hints_file)) unless hints_file.nil?
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,50 @@
1
+ module Psychic
2
+ class Runner
3
+ module SampleRunner
4
+ def run_sample(code_sample, *args)
5
+ sample_file = Psychic::Util.find_file_by_alias(code_sample, cwd)
6
+ process_template(sample_file) if templated?
7
+ command = command_for_task('run_sample')
8
+ if command
9
+ variables = { sample: code_sample, sample_file: sample_file }
10
+ command = Psychic::Util.replace_tokens(command, variables)
11
+ execute(command, *args)
12
+ else
13
+ run_sample_file(sample_file)
14
+ end
15
+ end
16
+
17
+ def run_sample_file(sample_file, *args)
18
+ execute("./#{sample_file}", *args) # Assuming Bash, but should detect Windows and use PowerShell
19
+ end
20
+
21
+ def process_template(sample_file)
22
+ absolute_sample_file = File.expand_path(sample_file, cwd)
23
+ template = File.read(absolute_sample_file)
24
+ # Default token pattern/replacement (used by php-opencloud) should be configurable
25
+ content = Psychic::Util.replace_tokens(template, variables, /'\{(\w+)\}'/, "'\\1'")
26
+
27
+ # Backup and overwrite
28
+ backup_file = "#{absolute_sample_file}.bak"
29
+ fail 'Please clear out old backups before rerunning' if File.exist? backup_file
30
+ FileUtils.cp(absolute_sample_file, backup_file)
31
+ File.write(absolute_sample_file, content)
32
+ end
33
+
34
+ def templated?
35
+ # Probably not the best way to turn this on/off
36
+ true unless variables.nil?
37
+ end
38
+
39
+ def variables
40
+ # ... or
41
+ variables_file = Dir["#{cwd}/psychic-variables.{yaml,yml}"].first
42
+ return nil unless variables_file
43
+ environment_variables = ENV.to_hash
44
+ environment_variables.merge!(@opts[:env]) if @opts[:env]
45
+ variables = Psychic::Util.replace_tokens(File.read(variables_file), environment_variables)
46
+ YAML.load(variables)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,5 @@
1
+ module Psychic
2
+ class Runner
3
+ VERSION = '0.0.2'
4
+ end
5
+ end
@@ -0,0 +1,37 @@
1
+ require 'psychic/runner/version'
2
+
3
+ autoload :YAML, 'yaml'
4
+
5
+ module Psychic
6
+ autoload :Util, 'psychic/util'
7
+ autoload :Logger, 'psychic/logger'
8
+ autoload :Shell, 'psychic/shell'
9
+ class Runner
10
+ autoload :BaseRunner, 'psychic/runner/base_runner'
11
+ autoload :SampleRunner, 'psychic/runner/sample_runner'
12
+ autoload :HotRunner, 'psychic/runner/hot_runner'
13
+ autoload :CompoundRunner, 'psychic/runner/compound_runner'
14
+ autoload :ColdRunnerRegistry, 'psychic/runner/cold_runner_registry'
15
+ class TaskNotImplementedError < NotImplementedError; end
16
+ ColdRunnerRegistry.autoload_runners!
17
+
18
+ include BaseRunner
19
+ include SampleRunner
20
+ attr_reader :runners, :hot_runner, :cold_runners
21
+
22
+ def initialize(opts = { cwd: Dir.pwd })
23
+ fail 'cwd is required' unless opts[:cwd]
24
+ super
25
+ @hot_runner = HotRunner.new(opts)
26
+ @cold_runners = ColdRunnerRegistry.active_runners(opts)
27
+ @runners = [@hot_runner, @cold_runners].flatten
28
+ @known_tasks = @runners.map(&:known_tasks).uniq
29
+ end
30
+
31
+ def [](task_name)
32
+ runner = runners.find { |r| r.command_for_task(task_name) }
33
+ return nil unless runner
34
+ runner[task_name]
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,42 @@
1
+ require 'English'
2
+
3
+ module Psychic
4
+ module Shell
5
+ class ExecutionError < StandardError
6
+ attr_accessor :execution_result
7
+ end
8
+
9
+ class ExecutionResult
10
+ attr_reader :exitstatus
11
+ attr_reader :stdout
12
+ attr_reader :stderr
13
+ # coerce_value String, ->(v) { v.force_encoding('utf-8') }
14
+
15
+ def initialize(results)
16
+ @exitstatus = results.fetch(:exitstatus)
17
+ # Needs to be UTF-8 to serialize as YAML
18
+ @stdout = results.fetch(:stdout).force_encoding('utf-8')
19
+ @stderr = results.fetch(:stderr).force_encoding('utf-8')
20
+ end
21
+
22
+ def error!
23
+ if @exitstatus != 0
24
+ error = ExecutionError.new
25
+ error.execution_result = self
26
+ fail error
27
+ end
28
+ end
29
+
30
+ def to_s
31
+ ''"
32
+ Execution Result:
33
+ exitstatus: #{exitstatus}
34
+ stdout:
35
+ #{stdout}
36
+ stderr:
37
+ #{stderr}
38
+ "''
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,62 @@
1
+ require 'mixlib/shellout'
2
+
3
+ module Psychic
4
+ module Shell
5
+ class IOToLog < IO
6
+ def initialize(logger)
7
+ @logger = logger
8
+ @buffer = ''
9
+ end
10
+
11
+ def write(string)
12
+ (@buffer + string).lines.each do |line|
13
+ if line.end_with? "\n"
14
+ @buffer = ''
15
+ @logger.info(line.rstrip)
16
+ else
17
+ @buffer = line
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ class MixlibShellOutExecutor
24
+ include Psychic::Logger
25
+ attr_reader :shell
26
+
27
+ MIXLIB_SHELLOUT_EXCEPTION_CLASSES = Mixlib::ShellOut.constants.map do|name|
28
+ klass = Mixlib::ShellOut.const_get(name)
29
+ if klass.is_a?(Class) && klass <= RuntimeError
30
+ klass
31
+ else
32
+ nil
33
+ end
34
+ end.compact
35
+
36
+ def execute(command, opts)
37
+ @logger = opts.delete(:logger) || logger
38
+ @shell = Mixlib::ShellOut.new(command, opts)
39
+ @shell.live_stream = IOToLog.new(@logger)
40
+ @shell.run_command
41
+ execution_result
42
+ rescue SystemCallError, *MIXLIB_SHELLOUT_EXCEPTION_CLASSES, TypeError => e
43
+ # See https://github.com/opscode/mixlib-shellout/issues/62
44
+ execution_error = ExecutionError.new(e)
45
+ execution_error.execution_result = execution_result
46
+ raise execution_error
47
+ end
48
+
49
+ private
50
+
51
+ def execution_result
52
+ return nil if shell.nil?
53
+
54
+ ExecutionResult.new(
55
+ exitstatus: shell.exitstatus,
56
+ stdout: shell.stdout,
57
+ stderr: shell.stderr
58
+ )
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,19 @@
1
+ module Psychic
2
+ module Shell
3
+ autoload :ExecutionResult, 'psychic/shell/execution_result'
4
+ autoload :ExecutionError, 'psychic/shell/execution_result'
5
+ autoload :MixlibShellOutExecutor, 'psychic/shell/mixlib_shellout_executor'
6
+
7
+ class << self
8
+ attr_writer :shell
9
+ end
10
+
11
+ def self.shell
12
+ @shell ||= MixlibShellOutExecutor.new
13
+ end
14
+
15
+ def shell
16
+ Psychic::Shell.shell
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,39 @@
1
+ autoload :Mustache, 'mustache'
2
+
3
+ module Psychic
4
+ class RegexpTokenHandler
5
+ def initialize(template, token_pattern, token_replacement)
6
+ @template = template
7
+ @token_pattern = token_pattern
8
+ @token_replacement = token_replacement
9
+ end
10
+
11
+ def tokens
12
+ @template.scan(@token_pattern).flatten.uniq
13
+ end
14
+
15
+ def replace(variables = {})
16
+ @template.gsub(@token_pattern) do
17
+ full_match = Regexp.last_match[0]
18
+ key = Regexp.last_match[1]
19
+ value = variables[key]
20
+ value = @token_replacement.gsub('\\1', value.to_s) unless @token_replacement.nil?
21
+ full_match.gsub(@token_pattern, value)
22
+ end
23
+ end
24
+ end
25
+
26
+ class MustacheTokenHandler
27
+ def initialize(template)
28
+ @template = Mustache::Template.new(template)
29
+ end
30
+
31
+ def tokens
32
+ @template.tags
33
+ end
34
+
35
+ def replace(variables = {})
36
+ Mustache.render(@template, variables)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,108 @@
1
+ module Psychic
2
+ autoload :RegexpTokenHandler, 'psychic/tokens'
3
+ autoload :MustacheTokenHandler, 'psychic/tokens'
4
+ class Util
5
+ # Returns a new Hash with all key values coerced to strings. All keys
6
+ # within a Hash are coerced by calling #to_s and hashes with arrays
7
+ # and other hashes are traversed.
8
+ #
9
+ # @param obj [Object] the hash to be processed. While intended for
10
+ # hashes, this method safely processes arbitrary objects
11
+ # @return [Object] a converted hash with all keys as strings
12
+ def self.stringified_hash(obj)
13
+ if obj.is_a?(Hash)
14
+ obj.each_with_object({}) do |(k, v), h|
15
+ h[k.to_s] = stringified_hash(v)
16
+ end
17
+ elsif obj.is_a?(Array)
18
+ obj.each_with_object([]) do |e, a|
19
+ a << stringified_hash(e)
20
+ end
21
+ else
22
+ obj
23
+ end
24
+ end
25
+
26
+ def self.relativize(file, base_path)
27
+ absolute_file = File.absolute_path(file)
28
+ absolute_base_path = File.absolute_path(base_path)
29
+ Pathname.new(absolute_file).relative_path_from Pathname.new(absolute_base_path)
30
+ end
31
+
32
+ def self.slugify(*labels)
33
+ labels.map do |label|
34
+ label.downcase.gsub(/[\.\s-]/, '_')
35
+ end.join('-')
36
+ end
37
+
38
+ def self.find_file_by_alias(file_alias, search_path, ignored_patterns = nil)
39
+ FileFinder.new(search_path, ignored_patterns).find_file(file_alias)
40
+ end
41
+
42
+ def self.replace_tokens(template, variables, token_regexp = nil, token_replacement = nil)
43
+ if token_regexp.nil?
44
+ MustacheTokenHandler.new(template).replace(variables)
45
+ else
46
+ RegexpTokenHandler.new(template, token_regexp, token_replacement).replace(variables)
47
+ end
48
+ end
49
+ end
50
+
51
+ class FileFinder
52
+ attr_reader :search_path, :ignored_patterns
53
+
54
+ def initialize(search_path, ignored_patterns)
55
+ @search_path = search_path
56
+ @ignored_patterns = ignored_patterns || read_gitignore(search_path)
57
+ end
58
+
59
+ # Finds a file by loosely matching the file name to a scenario name
60
+ def find_file(name)
61
+ return name if File.exist? File.expand_path(name, search_path)
62
+
63
+ # Filter out ignored filesFind the first file, not including generated files
64
+ files = potential_files(name).select do |f|
65
+ !ignored? f
66
+ end
67
+
68
+ # Select the shortest path, likely the best match
69
+ file = files.min_by(&:length)
70
+
71
+ fail Errno::ENOENT, "No file was found for #{name} within #{search_path}" if file.nil?
72
+ Psychic::Util.relativize(file, search_path)
73
+ end
74
+
75
+ def potential_files(name)
76
+ slugified_name = Psychic::Util.slugify(name)
77
+ glob_string = "#{search_path}/**/*#{slugified_name}*.*"
78
+ potential_files = Dir.glob(glob_string, File::FNM_CASEFOLD)
79
+ potential_files.concat Dir.glob(glob_string.gsub('_', '-'), File::FNM_CASEFOLD)
80
+ potential_files.concat Dir.glob(glob_string.gsub('_', ''), File::FNM_CASEFOLD)
81
+ end
82
+
83
+ private
84
+
85
+ # @api private
86
+ def read_gitignore(dir)
87
+ gitignore_file = "#{dir}/.gitignore"
88
+ File.read(gitignore_file)
89
+ rescue
90
+ ''
91
+ end
92
+
93
+ # @api private
94
+ def ignored?(target_file)
95
+ # Trying to match the git ignore rules but there's some discrepencies.
96
+ ignored_patterns.split.find do |pattern|
97
+ # if git ignores a folder, we should ignore all files it contains
98
+ pattern = "#{pattern}**" if pattern[-1] == '/'
99
+ started_with_slash = pattern.start_with? '/'
100
+
101
+ pattern.gsub!(%r{\A/}, '') # remove leading slashes since we're searching from root
102
+ file = Psychic::Util.relativize(target_file, search_path)
103
+ ignored = file.fnmatch? pattern
104
+ ignored || (file.fnmatch? "**/#{pattern}" unless started_with_slash)
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,33 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'psychic/runner/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'psychic-runner'
8
+ spec.version = Psychic::Runner::VERSION
9
+ spec.authors = ['Max Lincoln']
10
+ spec.email = ['max@devopsy.com']
11
+ spec.summary = 'Psychic runs anything.'
12
+ spec.description = 'Provides cross-project aliases for running tasks or similar code samples.'
13
+ # spec.homepage = ''
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_dependency 'thor', '~> 0.19'
22
+ spec.add_dependency 'mixlib-shellout', '~> 1.3' # Used for MRI
23
+ # spec.add_dependency "buff-shell_out", "~> 0.1" # Used for JRuby
24
+ spec.add_dependency 'mustache', '~> 0.99'
25
+ spec.add_development_dependency 'bundler', '~> 1.7'
26
+ spec.add_development_dependency 'rake', '~> 10.0'
27
+ spec.add_development_dependency 'rake-notes'
28
+ spec.add_development_dependency 'simplecov'
29
+ spec.add_development_dependency 'rspec', '~> 3.0'
30
+ spec.add_development_dependency 'rubocop', '~> 0.18', '<= 0.27'
31
+ spec.add_development_dependency 'rubocop-rspec', '~> 1.2'
32
+ spec.add_development_dependency 'aruba'
33
+ end
@@ -0,0 +1,80 @@
1
+ module Psychic
2
+ class Runner
3
+ module Cold
4
+ RSpec.describe ShellScriptRunner do
5
+ let(:shell) { Psychic::Shell.shell = double('shell') }
6
+ subject { described_class.new(cwd: current_dir) }
7
+
8
+ shared_context 'with scripts/*.sh files' do
9
+ before(:each) do
10
+ write_file 'scripts/bootstrap.sh', ''
11
+ write_file 'scripts/compile.sh', ''
12
+ write_file 'scripts/foo.ps1', ''
13
+ end
14
+ end
15
+
16
+ shared_context 'with scripts/* (no extension) files' do
17
+ before(:each) do
18
+ write_file 'scripts/bootstrap', ''
19
+ write_file 'scripts/compile', ''
20
+ write_file 'scripts/.foo', ''
21
+ end
22
+ end
23
+
24
+ describe 'respond_to?' do
25
+ shared_examples 'detects matching scripts' do
26
+ it 'returns true if a matching script exists' do
27
+ expect(subject.respond_to? :bootstrap).to be true
28
+ expect(subject.respond_to? :compile).to be true
29
+ end
30
+ it 'returns false if a matching script does not exists' do
31
+ expect(subject.respond_to? :foo).to be false
32
+ expect(subject.respond_to? :bar).to be false
33
+ end
34
+ end
35
+
36
+ context 'with scripts/*.sh files' do
37
+ include_context 'with scripts/*.sh files' do
38
+ include_examples 'detects matching scripts'
39
+ end
40
+ end
41
+
42
+ context 'with scripts/* (no extension) files' do
43
+ include_context 'with scripts/* (no extension) files' do
44
+ include_examples 'detects matching scripts'
45
+ end
46
+ end
47
+ end
48
+
49
+ describe '#method_missing' do
50
+ context 'matching a task' do
51
+ context 'with scripts/*.sh files' do
52
+ include_context 'with scripts/*.sh files' do
53
+ it 'executes the script command' do
54
+ expect(shell).to receive(:execute).with('./scripts/bootstrap.sh', cwd: current_dir)
55
+ subject.bootstrap
56
+ end
57
+ end
58
+ end
59
+
60
+ context 'with scripts/* (no extension) files' do
61
+ include_context 'with scripts/* (no extension) files' do
62
+ it 'executes the script command' do
63
+ expect(shell).to receive(:execute).with('./scripts/bootstrap', cwd: current_dir)
64
+ subject.bootstrap
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ context 'not matching a task' do
71
+ it 'raises an error' do
72
+ # Use foo to ensure it doesn't match ps1 or hidden (. prefixed) files
73
+ expect { subject.foo }.to raise_error(NoMethodError)
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,63 @@
1
+ module Psychic
2
+ class Runner
3
+ RSpec.describe HotRunner do
4
+ let(:task_map) do
5
+ {
6
+ 'bootstrap' => 'foo',
7
+ 'compile' => 'bar',
8
+ 'execute' => 'baz'
9
+ }
10
+ end
11
+ let(:shell) { Psychic::Shell.shell = double('shell') }
12
+ subject { described_class.new(cwd: current_dir, hints: task_map) }
13
+
14
+ shared_examples 'runs tasks' do
15
+ describe 'respond_to?' do
16
+ it 'returns true for task ids' do
17
+ task_map.each_key do |key|
18
+ expect(subject.respond_to? key).to be true
19
+ end
20
+ end
21
+
22
+ it 'returns false for anything else' do
23
+ expect(subject.respond_to? 'max').to be false
24
+ end
25
+ end
26
+
27
+ describe '#method_missing' do
28
+ context 'matching a task' do
29
+ it 'executes the task command' do
30
+ expect(shell).to receive(:execute).with('foo', cwd: current_dir)
31
+ subject.bootstrap
32
+ end
33
+ end
34
+
35
+ context 'not matching a task' do
36
+ it 'raises an error' do
37
+ expect { subject.spin_around }.to raise_error(NoMethodError)
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ context 'task map stored in psychic-hints.yml' do
44
+ let(:hints) do
45
+ { 'tasks' => task_map }
46
+ end
47
+ before(:each) do
48
+ write_file 'psychic-hints.yml', YAML.dump(hints)
49
+ end
50
+ subject { described_class.new(cwd: current_dir) }
51
+ include_examples 'runs tasks'
52
+ end
53
+
54
+ context 'hints passed as a parameter' do
55
+ let(:hints) do
56
+ { 'tasks' => task_map }
57
+ end
58
+ subject { described_class.new(cwd: current_dir, hints: hints) }
59
+ include_examples 'runs tasks'
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+
3
+ module Psychic
4
+ RSpec.describe Runner do
5
+ subject { described_class.new(cwd: current_dir) }
6
+ context 'when psychic-hints.yml exists' do
7
+ let(:hints) do
8
+ {
9
+ 'tasks' =>
10
+ {
11
+ 'bootstrap' => 'foo',
12
+ 'compile' => 'bar',
13
+ 'execute' => 'baz'
14
+ }
15
+ }
16
+ end
17
+
18
+ before(:each) do
19
+ write_file 'psychic-hints.yml', YAML.dump(hints)
20
+ end
21
+
22
+ describe 'initialize' do
23
+ it 'should create a HotRunner for the specified directory' do
24
+ expect(subject.hot_runner).to be_an_instance_of(Psychic::Runner::HotRunner)
25
+ expect(subject.cwd).to eq(current_dir)
26
+ end
27
+ end
28
+ end
29
+
30
+ context 'when scripts/* exist' do
31
+ before(:each) do
32
+ write_file 'scripts/bootstrap.sh', ''
33
+ write_file 'scripts/foo.sh', ''
34
+ end
35
+
36
+ describe 'initialize' do
37
+ it 'should create a cold runner for ShellScriptRunner' do
38
+ expect(subject.cold_runners).to include(
39
+ an_instance_of(Psychic::Runner::Cold::ShellScriptRunner)
40
+ )
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,47 @@
1
+ require 'simplecov'
2
+ SimpleCov.start
3
+
4
+ require 'rspec'
5
+ require 'psychic/runner'
6
+ require 'aruba'
7
+ require 'aruba/api'
8
+
9
+ # Config required for project
10
+ RSpec.configure do | config |
11
+ config.include Aruba::Api
12
+ config.before(:example) do
13
+ @aruba_timeout_seconds = 30
14
+ clean_current_dir
15
+ end
16
+ end
17
+
18
+ # Configs recommended by RSpec
19
+ RSpec.configure do |config|
20
+ config.warnings = true
21
+ config.disable_monkey_patching!
22
+ config.filter_run :focus
23
+ config.run_all_when_everything_filtered = true
24
+
25
+ config.expect_with :rspec do |expectations|
26
+ # This option will default to `true` in RSpec 4.
27
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
28
+ end
29
+
30
+ config.mock_with :rspec do |mocks|
31
+ # Prevents you from mocking or stubbing a method that does not exist on
32
+ # a real object. This is generally recommended, and will default to
33
+ # `true` in RSpec 4.
34
+ mocks.verify_partial_doubles = true
35
+ end
36
+
37
+ if config.files_to_run.one?
38
+ # Use the documentation formatter for detailed output,
39
+ # unless a formatter has already been configured
40
+ # (e.g. via a command-line flag).
41
+ config.default_formatter = 'doc'
42
+ end
43
+
44
+ config.profile_examples = 10
45
+ config.order = :random
46
+ Kernel.srand config.seed
47
+ end
metadata ADDED
@@ -0,0 +1,239 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: psychic-runner
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Max Lincoln
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-12-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: thor
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.19'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.19'
27
+ - !ruby/object:Gem::Dependency
28
+ name: mixlib-shellout
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.3'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: mustache
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.99'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.99'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.7'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.7'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '10.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '10.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake-notes
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: simplecov
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rspec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '3.0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '3.0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rubocop
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '0.18'
132
+ - - "<="
133
+ - !ruby/object:Gem::Version
134
+ version: '0.27'
135
+ type: :development
136
+ prerelease: false
137
+ version_requirements: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - "~>"
140
+ - !ruby/object:Gem::Version
141
+ version: '0.18'
142
+ - - "<="
143
+ - !ruby/object:Gem::Version
144
+ version: '0.27'
145
+ - !ruby/object:Gem::Dependency
146
+ name: rubocop-rspec
147
+ requirement: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - "~>"
150
+ - !ruby/object:Gem::Version
151
+ version: '1.2'
152
+ type: :development
153
+ prerelease: false
154
+ version_requirements: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - "~>"
157
+ - !ruby/object:Gem::Version
158
+ version: '1.2'
159
+ - !ruby/object:Gem::Dependency
160
+ name: aruba
161
+ requirement: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ version: '0'
166
+ type: :development
167
+ prerelease: false
168
+ version_requirements: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - ">="
171
+ - !ruby/object:Gem::Version
172
+ version: '0'
173
+ description: Provides cross-project aliases for running tasks or similar code samples.
174
+ email:
175
+ - max@devopsy.com
176
+ executables:
177
+ - psychic
178
+ extensions: []
179
+ extra_rdoc_files: []
180
+ files:
181
+ - ".gitignore"
182
+ - ".rspec"
183
+ - ".rubocop.yml"
184
+ - ".rubocop_todo.yml"
185
+ - ".travis.yml"
186
+ - Gemfile
187
+ - LICENSE.txt
188
+ - README.md
189
+ - Rakefile
190
+ - bin/psychic
191
+ - lib/psychic/cli.rb
192
+ - lib/psychic/commands/exec.rb
193
+ - lib/psychic/logger.rb
194
+ - lib/psychic/runner.rb
195
+ - lib/psychic/runner/base_runner.rb
196
+ - lib/psychic/runner/cold/shell_script_runner.rb
197
+ - lib/psychic/runner/cold_runner_registry.rb
198
+ - lib/psychic/runner/hot_runner.rb
199
+ - lib/psychic/runner/sample_runner.rb
200
+ - lib/psychic/runner/version.rb
201
+ - lib/psychic/shell.rb
202
+ - lib/psychic/shell/execution_result.rb
203
+ - lib/psychic/shell/mixlib_shellout_executor.rb
204
+ - lib/psychic/tokens.rb
205
+ - lib/psychic/util.rb
206
+ - psychic-runner.gemspec
207
+ - spec/psychic/runner/cold/shell_script_runner_spec.rb
208
+ - spec/psychic/runner/hot_runner_spec.rb
209
+ - spec/psychic/runner_spec.rb
210
+ - spec/spec_helper.rb
211
+ homepage:
212
+ licenses:
213
+ - MIT
214
+ metadata: {}
215
+ post_install_message:
216
+ rdoc_options: []
217
+ require_paths:
218
+ - lib
219
+ required_ruby_version: !ruby/object:Gem::Requirement
220
+ requirements:
221
+ - - ">="
222
+ - !ruby/object:Gem::Version
223
+ version: '0'
224
+ required_rubygems_version: !ruby/object:Gem::Requirement
225
+ requirements:
226
+ - - ">="
227
+ - !ruby/object:Gem::Version
228
+ version: '0'
229
+ requirements: []
230
+ rubyforge_project:
231
+ rubygems_version: 2.4.2
232
+ signing_key:
233
+ specification_version: 4
234
+ summary: Psychic runs anything.
235
+ test_files:
236
+ - spec/psychic/runner/cold/shell_script_runner_spec.rb
237
+ - spec/psychic/runner/hot_runner_spec.rb
238
+ - spec/psychic/runner_spec.rb
239
+ - spec/spec_helper.rb