#!/usr/bin/env ruby # frozen_string_literal: true # Repo-level test runner for the mixed minitest/pytest suites under plugins/. # # bin/test # run every plugin suite # bin/test os-backlog # run one plugin's suites (repeatable) # # Per plugin it runs `ruby tests/all.rb` when present (minitest) and # `pytest tests/` when any tests/*_test.py or tests/test_*.py exists. # pytest runs once per plugin so same-named test files (hook_test.py in # several plugins) never collide on module import. # os-vault's tests/ is an eval harness (harness.sh), not a unit suite, # and is intentionally not run here. require "open3" class PluginSuite RUNNERS = { minitest: ->(dir) { ["ruby", "tests/all.rb"] }, pytest: ->(dir) { ["pytest", "-q", "tests"] } }.freeze attr_reader :name def initialize(dir) @dir = dir @name = File.basename(dir) end def kinds kinds = [] kinds << :minitest if File.exist?(File.join(@dir, "tests", "all.rb")) kinds << :pytest if pytest_files? kinds end def run(kind) command = RUNNERS.fetch(kind).call(@dir) stdout, status = Open3.capture2e(*command, chdir: @dir) [status.success?, stdout] end private def pytest_files? Dir.glob(File.join(@dir, "tests", "{test_*,*_test}.py")).any? end end repo_root = File.expand_path("..", __dir__) requested = ARGV suites = Dir.glob(File.join(repo_root, "plugins", "*")) .select { |d| File.directory?(d) } .map { |d| PluginSuite.new(d) } .select { |s| requested.empty? || requested.include?(s.name) } .sort_by(&:name) unknown = requested - suites.map(&:name) abort "bin/test: no such plugin(s): #{unknown.join(", ")}" unless unknown.empty? failures = [] ran = 0 suites.each do |suite| suite.kinds.each do |kind| ran += 1 ok, output = suite.run(kind) puts "#{ok ? "PASS" : "FAIL"} #{suite.name} (#{kind})" unless ok failures << "#{suite.name} (#{kind})" puts output.lines.map { |l| " #{l}" }.join end end end if ran.zero? puts "bin/test: no test suites found" elsif failures.empty? puts "#{ran} suites passed" else abort "#{failures.size}/#{ran} suites failed: #{failures.join(", ")}" end