| # 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 |