The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/env ruby

#This script requires a standard directory hierarchy which might be a bit cumbersome to set up
#
#The hierarchy looks like this near the leaves:
#...
#|-test_subclass_1
#| |-test_1
#| | |-input.scss
#| | --expected_output.css
#| --test_2
#|   |-input.scss
#|   --expected_output.css
#|-test_subclass_2
#| |-test_1
#| | |-input.scss
#| | --expected_output.css
#...
#the point is to have all the tests in their own folder in a file named input* with
#the output of running a command on it in the file expected_output* in the same directory

#begin help section
def description()
	"\nThis script will search for all files under the current (or specified) directory that are\n"+
	"named input.scss. It will then run a specified binary and check that the output matches the\n"+
	"expected output. If you want set up your own test suite, follow a similar hierarchy as described in\n"+
	"the initial comment of this script for your test hierarchy. (This script is intended for testing\n"+
	"sass implementations, but can be easily modified to test anything.)\n\n"
end

def getusage()
	"Usage: testrunner.rb [Options]\n"+
	"\n"+
	"Options:\n"+
	"\t-c=, --command=\tSets a specific binary to run (defaults to 'sass')\n"+
	"\t-d=, --dir=\tSets the directory to recursively search for tests (defaults to '.')\n"+
	"\t-f, --fails\tDon't print out information about passing tests to make the output easier to wade through"+
	"\t-h, --help\t\tDisplay this message\n"+
	"\t--nolog\t\tDon't display the log of diffs after all the tests are run\n"+
	"\t-v, --verbose\tDisplay more info\n\n"
end

def exampleusage()
	"Example usage:\n"+
	"./testrunner.rb -c='sassc'\n"+
	"./testrunner.rb -d=mytestsuite -v\n\n"
end

#print an error message and the usage message, exit with a certain return code
def usage(s,c)
	$stderr.puts(s + getusage())
	exit(c)
end
#end help section

#begin option parsing/sanitizing section
opts = {}

opts[:cmd] = 'sass' #set to the default command
opts[:srchpath] = '.' #set to the default path
opts[:verbose] = false #don't be too talkative by default
opts[:skip] = false #if a command doesn't exit nicely, stop testing
opts[:fails] = false
opts[:nolog] = false

loop { case ARGV[0] #this argument parsing allows garbage at the end that doesn't start with '-', modify if necessary
	when /^-(c|-command)=/ then    #to change what to run, modify these lines (or copy them)
		opts[:cmd] = ARGV.shift.split("=",2)[1] #get the specified binary
		if (opts[:cmd] == "") #catch empty command
			usage("\nERROR: Must specify a command after -c= or --command=\n\n", 1)
		end
	when /^-(d|-dir)=/ then
		opts[:srchpath] = ARGV.shift.split("=",2)[1] #get the dir
		if (opts[:srchpath] == "") #catch empty dir (no dir was input)
			usage("\nERROR: Must specify a directory to search after -d= or --dir=\n\n", 1)
		end
	when /^-(f|-fails)$/ then
		opts[:fails] = true
		ARGV.shift
	when /^-(h|-help)$/ then
		puts description() + getusage() + exampleusage()
		exit(0)
	when /^--nolog$/ then
		opts[:nolog] = true
		ARGV.shift
	when /^-(s|-skip)$/ then
		opts[:skip] = true
		ARGV.shift
	when /^-(v|-verbose)$/ then
		opts[:verbose] = true
		ARGV.shift
	when /^-/ then #found an unhandled option, print an error and exit out
		usage("\nERROR: Unknown option: #{ARGV[0].inspect} (make sure to include the '=' for options that require it)\n\n", 2)
	else break
end; }

if !opts[:srchpath].end_with?("/") then 
	#add a "/" at the end if needed, necessary for globbing to find the needed files
	opts[:srchpath]+="/" 
end

#not strictly necessary test, but allows a more tailored error message
if !File.directory?(opts[:srchpath])
	$stderr.puts("\nERROR: Directory specified needs to be a directory. You specified #{opts[:srchpath]}.\n")
	exit(3)
end
#end option parsing/sanitizing section

#begin actual script
puts("Recursively searching under directory '#{opts[:srchpath]}' for test files to test '#{opts[:cmd]}' with.")

test_count = 0
worked = 0
did_not_run = 0
has_no_expected_output = 0
messages = []

Dir["#{opts[:srchpath]}**/input.scss"].each do |input_file|
	#test the file
	test_count += 1
	spec_dir = File.dirname(input_file)

	outfile = File.join(spec_dir, "output.out")
	expected_file = File.join(spec_dir, "expected_output.css")
	
	if !File.exists?(expected_file) #there is no expected_output.css file acompanying
		did_not_run += 1
		has_no_expected_output += 1
		$stderr.puts("ERROR: #{input_file} has no accompanying expected_output.css, skipping test.")
		next
	end

	`#{opts[:cmd]} #{input_file} > #{outfile} 2> /dev/null`

	if $?.to_i != 0 #cmd failed
		err_message = "Command '#{opts[:cmd]} #{input_file}' terminated unsuccessfully with error code #{$?.to_i}."
		$stderr.puts("ERROR: " + err_message)
		`rm "#{outfile}"`
		did_not_run += 1
		if !opts[:skip]
			$stderr.puts("Exiting, make sure '#{opts[:cmd]}' is available from your $PATH or use the -s or --skip option to skip tests that fail to exit successfully.")
			exit(4)
		end
		message = "Failed test in #{spec_dir}\n"
		message << err_message
		messages << message
		next
	end

	output_from_test = File.read(outfile)
	expected_output = File.read(expected_file)

	if expected_output.strip != output_from_test.strip
		if opts[:verbose]
			puts("Failed for #{input_file}.")
		else
			print "F"
		end
		message = "Failed test in #{spec_dir}\n"
		message << `diff -rub #{expected_file} #{outfile}`
		messages << message
	else
		worked += 1
		if !opts[:fails]
			if opts[:verbose]
				puts("Passed for #{input_file}.")
			else
				print "."
			end
		end
	end

	`rm "#{outfile}"`
end

if test_count == 0
	puts("No tests were run, please make sure this is the correct directory and it has input files under it somewhere unhidden.")
	exit(0)
else
	puts("")
	outmessage = "#{test_count} tests found. "
	if did_not_run > 0
		outmessage += "#{did_not_run} of them were not run"
		if has_no_expected_output == did_not_run
			outmessage += " due to not having an expected output.\n"
		elsif has_no_expected_output == 0
			outmessage += " due to unsuccessful termination.\n"
		else
			outmessage += ":\n#{has_no_expected_output} of them because of no expected output\n#{did_not_run - has_no_expected_output} of them because of unsuccessful termination.\n"
		end
	end
	outmessage += "Of the #{test_count - did_not_run} that ran, #{worked} passed."
	puts(outmessage)
end

if messages.length > 0
	if !opts[:nolog]
		puts("\n================================\nTEST FAILURES!\n\n")
		puts(messages.join("\n-----------\n"))
		puts("\n")
	end	
	exit(1)
else
	puts("GG, WP")
	exit(0)
end
#end actual script