require 'crack' require_relative 'pytest/requirements_checker' module LearnTest module Strategies class Pytest < LearnTest::Strategy def service_endpoint '/e/flatiron_pytest' end def detect test_files.count > 0 end def check_dependencies LearnTest::Pytest::RequirementsChecker.check_installation end def run system("python -m pytest #{options[:argv].join(' ')} --junitxml='./.results.xml'") end def output @output ||= Crack::XML.parse(File.read('.results.xml'))["testsuite"] end def test_files # pytest will run all files of the form test_*.py or *_test.py @test_files ||= Dir.glob("**/test_*.py") + Dir.glob("**/*_test.py") end def results failed_count = output["failures"].to_i + output["errors"].to_i skipped_count = output["skips"].to_i passed_count = output["tests"].to_i - failed_count - skipped_count { username: username, github_user_id: user_id, learn_oauth_token: learn_oauth_token, repo_name: runner.repo, build: { test_suite: [{ framework: 'pytest', formatted_output: output.to_json, duration: output["time"] }] }, examples: output["tests"].to_i, passing_count: passed_count, pending_count: skipped_count, failure_count: failed_count, failure_descriptions: concat_failure_descriptions } end def cleanup FileUtils.rm('.results.xml') end private def calculate_duration output[:results].map do |example| example[:time] end.inject(:+) end def concat_failure_descriptions # if there is a single test the `testcase` xml parse turns out a hash # instead of an array with a single hash. this will make sure single # tests have the same output structure (Array) as multiple tests output["testcase"] = [output["testcase"]].flatten output["testcase"].reduce([]) do |errors, example| if example.has_key?("failure") errors << example.map{|k, v| "#{k}: #{v}"} end errors end end end end end