blob: 78ebc6feecbd46017df6a4143a468690287eeaa8 [file] [log] [blame]
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with this
# work for additional information regarding copyright ownership. The ASF
# licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
module Buildr #:nodoc:
# Methods added to Project to allow checking the build.
module Checks
module Matchers #:nodoc:
class << self
# Define matchers that operate by calling a method on the tested object.
# For example:
# foo.should contain(bar)
# calls:
# foo.contain(bar)
def match_using(*names)
names.each do |name|
matcher = Class.new do
# Initialize with expected arguments (i.e. contain(bar) initializes with bar).
define_method(:initialize) { |*args| @expects = args }
# Matches against actual value (i.e. foo.should exist called with foo).
define_method(:matches?) do |actual|
@actual = actual
return actual.send("#{name}?", *@expects) if actual.respond_to?("#{name}?")
return actual.send(name, *@expects) if actual.respond_to?(name)
raise "You can't check #{actual}, it doesn't respond to #{name}."
end
# Some matchers have arguments, others don't, treat appropriately.
define_method :failure_message do
args = " " + @expects.map{ |arg| "'#{arg}'" }.join(", ") unless @expects.empty?
"Expected #{@actual} to #{name}#{args}"
end
define_method :negative_failure_message do
args = " " + @expects.map{ |arg| "'#{arg}'" }.join(", ") unless @expects.empty?
"Expected #{@actual} to not #{name}#{args}"
end
end
# Define method to create matcher.
define_method(name) { |*args| matcher.new(*args) }
end
end
end
# Define delegate matchers for exist and contain methods.
match_using :exist, :contain
end
# An expectation has subject, description and block. The expectation is validated by running the block,
# and can access the subject from the method #it. The description is used for reporting.
#
# The expectation is run by calling #run_against. You can share expectations by running them against
# different projects (or any other context for that matter).
#
# If the subject is missing, it is set to the argument of #run_against, typically the project itself.
# If the description is missing, it is set from the project. If the block is missing, the default behavior
# prints "Pending" followed by the description. You can use this to write place holders and fill them later.
class Expectation
attr_reader :description, :subject, :block
# :call-seq:
# initialize(subject, description?) { .... }
# initialize(description?) { .... }
#
# First argument is subject (returned from it method), second argument is description. If you omit the
# description, it will be set from the subject. If you omit the subject, it will be set from the object
# passed to run_against.
def initialize(*args, &block)
@description = args.pop if String === args.last
@subject = args.shift
raise ArgumentError, 'Expecting subject followed by description, and either one is optional. Not quite sure what to do with this list of arguments.' unless args.empty?
@block = block || lambda { |klass| info "Pending: #{description}" }
end
# :call-seq:
# run_against(context)
#
# Runs this expectation against the context object. The context object is different from the subject,
# but used as the subject if no subject specified (i.e. returned from the it method).
#
# This method creates a new context object modeled after the context argument, but a separate object
# used strictly for running this expectation, and used only once. The context object will pass methods
# to the context argument, so you can call any method, e.g. package(:jar).
#
# It also adds all matchers defined in Buildr and RSpec, and two additional methods:
# * it() -- Returns the subject.
# * description() -- Returns the description.
def run_against(context)
subject = @subject || context
description = @description ? "#{subject} #{@description}" : subject.to_s
# Define anonymous class and load it with:
# - All instance methods defined in context, so we can pass method calls to the context.
# - it() method to return subject, description() method to return description.
# - All matchers defined by Buildr and RSpec.
klass = Class.new
klass.instance_eval do
context.class.instance_methods.each do |method|
define_method(method) { |*args| context.send(method, *args) } unless instance_methods.include?(method)
end
define_method(:it) { subject }
define_method(:description) { description }
include ::RSpec::Matchers
include Matchers
end
# Run the expectation. We only print the expectation name when tracing (to know they all ran),
# or when we get a failure.
begin
trace description
klass.new.instance_eval &@block
rescue Exception=>error
raise error.exception("#{description}\n#{error}").tap { |wrapped| wrapped.set_backtrace(error.backtrace) }
end
end
end
include Extension
before_define(:check => :package) do |project|
# The check task can do any sort of interesting things, but the most important is running expectations.
project.task("check") do |task|
project.expectations.inject(true) do |passed, expect|
begin
expect.run_against project
passed
rescue Exception=>ex
if verbose
error ex
error ex.backtrace.select { |line| line =~ /#{Buildr.application.buildfile}/ }.join("\n")
end
false
end
end or fail "Checks failed for project #{project.name} (see errors above)."
end
project.task('package').enhance do |task|
# Run all actions before checks.
task.enhance { project.task('check').invoke }
end
end
# :call-seq:
# check(description) { ... }
# check(subject, description) { ... }
#
# Adds an expectation. The expectation is run against the project by the check task, executed after packaging.
# You can access any package created by the project.
#
# An expectation is written using a subject, description and block to validate the expectation. For example:
#
# For example:
# check package(:jar), "should exist" do
# it.should exist
# end
# check package(:jar), "should contain a manifest" do
# it.should contain("META-INF/MANIFEST.MF")
# end
# check package(:jar).path("com/acme"), "should contain classes" do
# it.should_not be_empty
# end
# check package(:jar).entry("META-INF/MANIFEST"), "should be a recent license" do
# it.should contain(/Copyright (C) 2007/)
# end
#
# If you omit the subject, the project is used as the subject. If you omit the description, the subject is
# used as description.
#
# During development you can write placeholder expectations by omitting the block. This will simply report
# the expectation as pending.
def check(*args, &block)
Buildr.ensure_rspec('check() method invoked in buildfile')
expectations << Checks::Expectation.new(*args, &block)
end
# :call-seq:
# expectations() => Expectation*
#
# Returns a list of expectations (see #check).
def expectations()
@expectations ||= []
end
end
end
module Rake #:nodoc:
class FileTask
# :call-seq:
# exist?() => boolean
#
# Returns true if this file exists.
def exist?()
File.exist?(name)
end
# :call-seq:
# empty?() => boolean
#
# Returns true if file/directory is empty.
def empty?()
File.directory?(name) ? Dir.glob("#{name}/*").empty? : File.read(name).empty?
end
# :call-seq:
# contain?(pattern*) => boolean
# contain?(file*) => boolean
#
# For a file, returns true if the file content matches against all the arguments. An argument may be
# a string or regular expression.
#
# For a directory, return true if the directory contains the specified files. You can use relative
# file names and glob patterns (using *, **, etc).
def contain?(*patterns)
if File.directory?(name)
patterns.map { |pattern| "#{name}/#{pattern}" }.all? { |pattern| !Dir[pattern].empty? }
else
contents = File.read(name)
patterns.map { |pattern| Regexp === pattern ? pattern : Regexp.new(Regexp.escape(pattern.to_s)) }.
all? { |pattern| contents =~ pattern }
end
end
end
end
class Buildr::Project #:nodoc:
include Buildr::Checks
end