diff --git a/Gemfile.lock b/Gemfile.lock index 934f84c..5b63ee5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -52,6 +52,7 @@ GEM PLATFORMS arm64-darwin-21 + ruby DEPENDENCIES advent! diff --git a/README.md b/README.md index 6165886..d494cd7 100644 --- a/README.md +++ b/README.md @@ -16,15 +16,26 @@ If bundler is not being used to manage dependencies, install the gem by executin ## Usage +Initialise a new project somewhere: + +```bash +mkdir advent_of_code && cd advent_of_code + +# create a blank advent.yml config file +advent init +``` + +Configuration values and format are explained in the [Config](#config) section. + Advent expects you to have a working directory resembling something like: $ tree . ├── 2015 - └── 2016 + ├── 2016 + └── advent.yml -Some commands can be run from within a directory for a specific year, but it's -better to run from the parent directory where possible. +You can run commands from anywhere under this directory. The typical flow for tackling a daily challenge would be: @@ -40,6 +51,32 @@ A list of commands and help is available using `advent`: $ advent help +## Config + +The config file should be at the root of your working directory called +`advent.yml`. The default values if you don't provide an override are: + +```yaml +download_when_generating: true +remember_session: true +``` + +### Config explained + +
+
download_when_generating
+
+When you run `advent generate` it will automatically download the input file to +go with it +
+ +
remember_session
+
+Save your session cookie in `.advent_session` when prompted so you don't need to +find it again +
+
+ ## Development After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. diff --git a/advent.gemspec b/advent.gemspec index bb250d6..7a8ec47 100644 --- a/advent.gemspec +++ b/advent.gemspec @@ -19,4 +19,12 @@ Gem::Specification.new do |spec| spec.require_paths = ["lib"] spec.add_dependency "thor", "~> 1.2" + + spec.post_install_message = " +advent v0.1.5 requires a config file in your working directory. + +See #{spec.homepage}/blob/main/README.md#usage or if you're +brave run `advent init` in your current directory. + +" end diff --git a/lib/advent.rb b/lib/advent.rb index 3e8ab1f..c74411a 100644 --- a/lib/advent.rb +++ b/lib/advent.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require_relative "advent/configuration" require_relative "advent/input" require_relative "advent/session" require_relative "advent/solution" @@ -10,8 +11,29 @@ module Advent class Error < StandardError; end class << self + def config + @_config ||= Configuration.from_file(root.join(Configuration::FILE_NAME)) + end + + def root + if (location = find_config_location) + location + else + raise Error, "Cannot find advent.yml config file in current or parent directories." + end + end + def session - @_session = Session.new + @_session ||= Session.new + end + + private + + def find_config_location + Pathname.new(Dir.pwd).ascend do |path| + return path if File.exist? path.join(Configuration::FILE_NAME) + return nil if path.to_s == "/" + end end end end diff --git a/lib/advent/cli.rb b/lib/advent/cli.rb index 8b6a1b5..60bb56b 100644 --- a/lib/advent/cli.rb +++ b/lib/advent/cli.rb @@ -9,14 +9,17 @@ module Advent class CLI < Thor include Thor::Actions - class_option :root_path, default: Dir.pwd, hide: true, check_default_type: false class_option :http_module, default: Net::HTTP, check_default_type: false def initialize(*args) super - self.destination_root = root_path source_paths << File.expand_path("templates", __dir__) + + # Don't try to load Advent.root if we're running init + unless args.last[:current_command]&.name == "init" + self.destination_root = Advent.root + end end # @return [Boolean] defines whether an exit status is set if a command fails @@ -24,44 +27,12 @@ def self.exit_on_failure? true end - no_commands do - # @return [Boolean] whether the current root_path option is in a - # directory that looks like a year (eg. 2015) - def in_year_directory? - dir = root_path.basename.to_s - dir =~ /^20[0-9]{2}/ - end - end - desc "download YEAR DAY", "Download the input for YEAR and DAY" - def download(year_or_day, day = nil) - year, day = determine_year_and_day(year_or_day, day) - - if (error_message = validate(year, day)) - say_error error_message, :red - return - end + def download(year, day) + require "advent/cli/downloader" - subpath = if in_year_directory? - "" - else - "#{year}/" - end - - unless Advent.session.exist? - session = ask "What is your Advent of Code session cookie value?", echo: false - Advent.session.value = session - - say "\n\nThanks. Psst, we're going to save this for next time. It's in .advent_session if you need to update or delete it.\n\n" - end - - input = Advent::Input.new(root_path.join(subpath), day: day.to_i) - - if input.download(Advent.session.value, options.http_module) - say "Input downloaded to #{input.file_path}.", :green - say "\nUsing #load_input in your daily solution will load the input file for you." - else - say_error "Something went wrong, maybe an old session cookie?", :red + Dir.chdir Advent.root do + Downloader.new(self, year, day).download end end @@ -69,29 +40,37 @@ def download(year_or_day, day = nil) # Generates a new solution file. If within a year directory, only the day # is used, otherwise both the year and day will be required to generate the # output. - def generate(year_or_day, day = nil) - year, day = determine_year_and_day(year_or_day, day) + def generate(year, day) + year = parse_number year + day = parse_number day - if (error_message = validate(year, day)) - say_error error_message, :red + if (message = validate(year, day)) + say_error message, :red return end - subpath = if in_year_directory? + template "solution.rb.tt", "#{year}/day#{day}.rb", context: binding + template "solution_test.rb.tt", "#{year}/test/day#{day}_test.rb", context: binding + + download year, day if Advent.config.download_when_generating + end + + desc "init DIR", "Initialise a new advent project in DIR" + def init(dir = ".") + create_file Pathname.getwd.join(dir).join(Advent::Configuration::FILE_NAME) do "" - else - "#{year}/" end - - template "solution.rb.tt", "#{subpath}day#{day}.rb", context: binding - template "solution_test.rb.tt", "#{subpath}test/day#{day}_test.rb", context: binding end desc "solve FILE", "Solve your solution" # Runs a solution file, outputting both :part1 and :part2 method return values. def solve(path) require "advent/cli/solver" - Solver.new(self, root_path.join(path)).solve + file_path = Pathname.getwd.join(path) + + Dir.chdir Advent.root do + Solver.new(self, file_path.relative_path_from(Advent.root)).solve + end end desc "version", "Prints the current version of the gem" @@ -102,22 +81,6 @@ def version private - def determine_year_and_day(year_or_day, day) - if in_year_directory? - [root_path.basename.to_s, parse_number(year_or_day)] - else - [year_or_day, parse_number(day)] - end - end - - def root_path - @_root_path ||= if options.root_path.is_a?(Pathname) - options.root_path - else - Pathname.new(options.root_path) - end - end - def parse_number(str) if (m = str.match(/[0-9]+/)) m[0] diff --git a/lib/advent/cli/downloader.rb b/lib/advent/cli/downloader.rb new file mode 100644 index 0000000..9c88277 --- /dev/null +++ b/lib/advent/cli/downloader.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class Advent::CLI::Downloader + def initialize(command, year, day) + @command = command + @year = year + @day = day + end + + def download + ask_for_session_cookie_if_needed + input = Advent::Input.new(Advent.root.join(@year), day: @day.to_i) + + if input.download(Advent.session.value, @command.options.http_module) + @command.say "Input downloaded to #{input.file_path}.", :green + @command.say "\nUsing #load_input in your daily solution will load the input file for you." + else + @command.say_error "Something went wrong, maybe an old session cookie?", :red + end + end + + private + + def ask_for_session_cookie_if_needed + return if Advent.session.exist? + + session = @command.ask "What is your Advent of Code session cookie value?", echo: false + Advent.session.value = session + + @command.say "\n\nThanks. Psst, we're going to save this for next time. It's in .advent_session if you need to update or delete it.\n\n" + end +end diff --git a/lib/advent/cli/solver.rb b/lib/advent/cli/solver.rb index f347351..a45e376 100644 --- a/lib/advent/cli/solver.rb +++ b/lib/advent/cli/solver.rb @@ -18,7 +18,7 @@ def solve load @path, Solutions solution = Solutions.const_get(solution_class_name).new else - require @path + require @path.expand_path solution = Object.const_get(solution_class_name).new end @@ -37,15 +37,11 @@ def solve private - def day - @_day ||= @path.basename.to_s.match(/day([0-9]+)\.rb/)[1] - end - - def solution_file_name - "day#{day}.rb" - end - def solution_class_name "Day#{day}" end + + def day + @_day ||= @path.basename.to_s.match(/day([0-9]+)\.rb/)[1] + end end diff --git a/lib/advent/configuration.rb b/lib/advent/configuration.rb new file mode 100644 index 0000000..90d246c --- /dev/null +++ b/lib/advent/configuration.rb @@ -0,0 +1,30 @@ +require "psych" + +module Advent + class Configuration + DEFAULTS = { + "download_when_generating" => true, + "remember_session" => true + } + FILE_NAME = "advent.yml" + + attr_reader :download_when_generating, :remember_session + + class << self + def from_file(file = FILE_NAME) + if RUBY_VERSION >= "3.1" + new Psych.safe_load_file(file) + else + new Psych.safe_load(File.read(file)) + end + end + end + + def initialize(conf) + config = DEFAULTS.merge(conf || {}) + + @download_when_generating = config.dig("download_when_generating") + @remember_session = config.dig("remember_session") + end + end +end diff --git a/lib/advent/session.rb b/lib/advent/session.rb index e2c2f15..348b973 100644 --- a/lib/advent/session.rb +++ b/lib/advent/session.rb @@ -18,11 +18,22 @@ def exist? end def value=(val) - File.write file_name, val + if save_to_disk? + File.write file_name, val + else + @_value = val + end end def value - File.read file_name if exist? + return @_value unless save_to_disk? + return File.read(file_name) if exist? + end + + private + + def save_to_disk? + Advent.config.remember_session end end end diff --git a/test/advent/cli_test.rb b/test/advent/cli_test.rb index 66128b2..4be6c2f 100644 --- a/test/advent/cli_test.rb +++ b/test/advent/cli_test.rb @@ -26,8 +26,29 @@ def setup "day 5 input" ) - @cli = Advent::CLI.new([], root_path: DUMMY_ROOT_PATH, http_module: http_mock) - @year_cli = Advent::CLI.new([], root_path: DUMMY_ROOT_PATH.join("2015")) + Dir.chdir DUMMY_ROOT_PATH + @cli = Advent::CLI.new([], http_module: http_mock) + end + + def teardown + Dir.chdir DUMMY_ROOT_PATH + Advent.session.clear + end + + def test_init + path = DUMMY_ROOT_PATH.join("init") + Dir.mkdir path + + out, _err = capture_io do + Dir.chdir path do + @cli.invoke(:init) + end + end + + assert_match(/create.*advent.yml/, out.strip) + ensure + File.delete path.join("advent.yml") + Dir.rmdir path end def test_version @@ -38,7 +59,7 @@ def test_version assert_equal Advent::VERSION, out.strip end - def test_solve_from_parent_directory + def test_solve_from_root_directory out, _err = capture_io do @cli.invoke(:solve, ["2015/day1.rb"]) end @@ -48,13 +69,15 @@ def test_solve_from_parent_directory def test_solve_from_year_directory out, _err = capture_io do - @year_cli.invoke(:solve, ["day2.rb"]) + Dir.chdir DUMMY_ROOT_PATH.join("2015") do + @cli.invoke(:solve, ["day2.rb"]) + end end assert_equal "Part 1: 789\nPart 2: Missing", out.strip end - def test_generate_solution_with_year_and_day + def test_generate capture_io do @cli.invoke(:generate, ["2015", "3"]) end @@ -67,22 +90,26 @@ def test_generate_solution_with_year_and_day end end - def test_generate_solution_from_year_directory_with_day - capture_io do - @year_cli.invoke(:generate, ["3"]) + def test_download_when_generating + with_session("abc123") do + with_config({"download_when_generating" => true}) do + capture_io do + @cli.invoke(:generate, ["2015", "3"]) + end + end end - ["day3.rb", "test/day3_test.rb"].each do |file_name| - expected_file_path = DUMMY_ROOT_PATH.join("2015", file_name) - assert File.exist? expected_file_path + assert File.exist? DUMMY_ROOT_PATH.join("2015", "day3.rb") + assert File.exist? DUMMY_ROOT_PATH.join("2015", ".day3.input.txt") - File.delete expected_file_path - end + File.delete DUMMY_ROOT_PATH.join("2015", "day3.rb") + File.delete DUMMY_ROOT_PATH.join("2015", "test", "day3_test.rb") + File.delete DUMMY_ROOT_PATH.join("2015", ".day3.input.txt") end - def test_generate_solution_with_non_numbers_in_day + def test_generate_day_parsing capture_io do - @year_cli.invoke(:generate, ["day3"]) + @cli.invoke(:generate, ["2015", "day3"]) end ["day3.rb", "test/day3_test.rb"].each do |file_name| @@ -93,7 +120,7 @@ def test_generate_solution_with_non_numbers_in_day end end - def test_generate_solution_valid_minimum_year + def test_generate_valid_minimum_year _out, err = capture_io do @cli.invoke(:generate, ["2013", "1"]) end @@ -101,15 +128,15 @@ def test_generate_solution_valid_minimum_year assert_equal "Advent of Code only started in 2014!", err.strip end - def test_generate_solution_valid_maximum_year + def test_generate_valid_maximum_year _out, err = capture_io do - @cli.invoke(:generate, [Date.today.year + 1, "1"]) + @cli.invoke(:generate, [(Date.today.year + 1).to_s, "1"]) end assert_equal "Future years are not supported.", err.strip end - def test_generate_solution_valid_minimum_day + def test_generate_valid_minimum_day _out, err = capture_io do @cli.invoke(:generate, ["2015", "0"]) end @@ -117,7 +144,7 @@ def test_generate_solution_valid_minimum_day assert_equal "Day must be between 1 and 25 (inclusive).", err.strip end - def test_generate_solution_valid_maximum_day + def test_generate_valid_maximum_day _out, err = capture_io do @cli.invoke(:generate, ["2015", "26"]) end @@ -125,9 +152,9 @@ def test_generate_solution_valid_maximum_day assert_equal "Day must be between 1 and 25 (inclusive).", err.strip end - def test_download_input + def test_download_from_root_directory out, _err = capture_io do - with_stdin_input(@session) do + with_session(@session) do @cli.invoke(:download, ["2015", "3"]) end end @@ -137,10 +164,10 @@ def test_download_input assert_equal "day 3 input", File.read(input_path) assert_match(/Input downloaded to #{input_path}/, out.strip) ensure - File.delete input_path + File.delete input_path if File.exist? input_path end - def test_download_persists_session_cookie + def test_persisting_session_cookie with_stdin_input(@session) do capture_io do @cli.invoke(:download, ["2015", "4"]) @@ -149,11 +176,11 @@ def test_download_persists_session_cookie assert_equal @session, Advent.session.value ensure - File.delete DUMMY_ROOT_PATH.join("2015", ".day4.input.txt") - Advent.session.clear + input = DUMMY_ROOT_PATH.join("2015", ".day4.input.txt") + File.delete input if File.exist? input end - def test_download_skips_asking_for_session_cookie + def test_not_asking_for_session_cookie_again Advent.session.value = @session out, _err = capture_io do @@ -164,22 +191,20 @@ def test_download_skips_asking_for_session_cookie refute_match(/session cookie value/, out) ensure - File.delete DUMMY_ROOT_PATH.join("2015", ".day5.input.txt") - Advent.session.clear + input = DUMMY_ROOT_PATH.join("2015", ".day5.input.txt") + File.delete input if File.exist? input end - def test_download_input_failure + def test_download_error _out, err = capture_io do - with_stdin_input("invalid") do + with_session("invalid") do @cli.invoke(:download, ["2015", "6"]) end end - input_path = DUMMY_ROOT_PATH.join("2015", ".day6.input.txt") - refute File.exist?(input_path) assert_match(/Something went wrong/, err.strip) ensure - File.delete input_path if File.exist? input_path - Advent.session.clear + input = DUMMY_ROOT_PATH.join("2015", ".day6.input.txt") + File.delete input_path if File.exist? input end end diff --git a/test/advent/configuration_test.rb b/test/advent/configuration_test.rb new file mode 100644 index 0000000..8f11e77 --- /dev/null +++ b/test/advent/configuration_test.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require "test_helper" + +class Advent::ConfigurationTest < Advent::TestCase + def setup + @config_file = DUMMY_ROOT_PATH.join("advent.yml") + end + + def test_config_is_merged_with_defaults + config = Advent::Configuration.new({}) + + assert config.download_when_generating + assert config.remember_session + end + + def test_initializing_from_file + config = Advent::Configuration.from_file(@config_file) + + refute config.download_when_generating + assert config.remember_session + end +end diff --git a/test/advent/session_test.rb b/test/advent/session_test.rb index bec1ba3..92f6ecc 100644 --- a/test/advent/session_test.rb +++ b/test/advent/session_test.rb @@ -2,6 +2,7 @@ class Advent::SessionTest < Advent::TestCase def setup + Dir.chdir DUMMY_ROOT_PATH @session = Advent::Session.new(name) end @@ -26,6 +27,15 @@ def test_setting_value assert_equal "session value", File.read(@session.file_name) end + def test_setting_value_honouring_config + with_config({"remember_session" => false}) do + @session.value = "session value" + + assert_equal "session value", @session.value + refute @session.exist? + end + end + def test_value assert_nil @session.value File.write @session.file_name, "another session value" diff --git a/test/advent_test.rb b/test/advent_test.rb index 46b6ea8..6de001c 100644 --- a/test/advent_test.rb +++ b/test/advent_test.rb @@ -3,6 +3,34 @@ require "test_helper" class TestAdvent < Advent::TestCase + def teardown + Dir.chdir DUMMY_ROOT_PATH + end + + def test_finding_root_from_config_location + Dir.chdir DUMMY_ROOT_PATH do + assert_equal DUMMY_ROOT_PATH, Advent.root + end + + Dir.chdir DUMMY_ROOT_PATH.join("2015") do + assert_equal DUMMY_ROOT_PATH, Advent.root + end + + Dir.chdir DUMMY_ROOT_PATH.join("..") do + error = assert_raises Advent::Error do + Advent.root + end + + assert_equal "Cannot find advent.yml config file in current or parent directories.", error.message + end + end + + def test_config_initialization + Dir.chdir DUMMY_ROOT_PATH do + assert_kind_of Advent::Configuration, Advent.config + end + end + def test_that_it_has_a_version_number refute_nil ::Advent::VERSION end diff --git a/test/dummy/advent.yml b/test/dummy/advent.yml new file mode 100644 index 0000000..e89a55d --- /dev/null +++ b/test/dummy/advent.yml @@ -0,0 +1,2 @@ +download_when_generating: false +remember_session: true diff --git a/test/test_helper.rb b/test/test_helper.rb index 901ce7f..ee8e04d 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -8,6 +8,24 @@ DUMMY_ROOT_PATH = Pathname.new File.expand_path("dummy", __dir__) class Advent::TestCase < Minitest::Test + def with_config(config) + original_config = Advent.config + + config_with_defaults = Advent::Configuration::DEFAULTS.merge(config) + Advent.instance_variable_set(:@_config, Advent::Configuration.new(config_with_defaults)) + + yield + ensure + Advent.instance_variable_set(:@_config, original_config) + end + + def with_session(value) + Advent.session.value = value + yield + ensure + Advent.session.clear + end + def with_stdin_input(input) require "stringio"