Commit 732ccc98 authored by Rene Saarsoo's avatar Rene Saarsoo
Browse files

Add tests for command line options.

Improve NullObject with the ability of values to be Proc's, which
will be invoked to provide more complicated logic.

Give names for all validators, so we can call them out by name in our
tests.

Add validator for :processes option.
parent 0c5d4eb7
Loading
Loading
Loading
Loading
+28 −20
Original line number Diff line number Diff line
@@ -13,10 +13,13 @@ module JsDuck
    # Performs parsing of JSDuck options.
    class Parser

      def initialize
      def initialize(file_class=File, config_class=Options::Config)
        @file = file_class
        @config = config_class

        @opts = Options::Record.new

        @root_dir = File.dirname(File.dirname(File.dirname(File.dirname(__FILE__))))
        @root_dir = @file.dirname(@file.dirname(@file.dirname(@file.dirname(__FILE__))))

        @optparser = Options::HelpfulParser.new
        init_parser
@@ -48,7 +51,7 @@ module JsDuck
        separator ""

        attribute :input_files, []
        validator do
        validator :input_files do
          if @opts.input_files.empty? && !@opts.welcome && !@opts.guides && !@opts.videos && !@opts.examples
            "Please specify some input files, otherwise there's nothing I can do :("
          end
@@ -68,7 +71,7 @@ module JsDuck
            @opts.cache_dir = @opts.output_dir + "/.cache" unless @opts.cache_dir
          end
        end
        validator do
        validator :output_dir do
          if @opts.output_dir == :stdout
            # No output dir needed for export
            if !@opts.export
@@ -76,9 +79,9 @@ module JsDuck
            end
          elsif !@opts.output_dir
            "Please specify an output directory, where to write all this amazing documentation"
          elsif File.exists?(@opts.output_dir) && !File.directory?(@opts.output_dir)
          elsif @file.exists?(@opts.output_dir) && !@file.directory?(@opts.output_dir)
            "The output directory is not really a directory at all :("
          elsif !File.exists?(File.dirname(@opts.output_dir))
          elsif !@file.exists?(@file.dirname(@opts.output_dir))
            "The parent directory for #{@opts.output_dir} doesn't exist"
          end
        end
@@ -94,7 +97,7 @@ module JsDuck
          "- examples - inline examples from classes and guides.") do |format|
          @opts.export = format.to_sym
        end
        validator do
        validator :export do
          if ![nil, :full, :examples].include?(@opts.export)
            "Unknown export format: #{@opts.export}"
          end
@@ -136,7 +139,7 @@ module JsDuck
          "",
          "See also: https://github.com/senchalabs/jsduck/wiki/Config-file") do |path|
          path = canonical(path)
          if File.exists?(path)
          if @file.exists?(path)
            config = read_json_config(path)
          else
            Logger.fatal("The config file #{path} doesn't exist")
@@ -144,7 +147,7 @@ module JsDuck
          end
          # treat paths inside JSON config relative to the location of
          # config file.  When done, switch back to current working dir.
          @working_dir = File.dirname(path)
          @working_dir = @file.dirname(path)
          parse_options(config)
          @working_dir = nil
          @opts.config = path
@@ -286,7 +289,7 @@ module JsDuck
          "6 - <H2>,<H3>,<H4>,<H5>,<H6> headings are included.") do |level|
          @opts.guides_toc_level = level.to_i
        end
        validator do
        validator :guides_toc_level do
          if !(1..6).include?(@opts.guides_toc_level)
            "Unsupported --guides-toc-level: '#{@opts.guides_toc_level}'"
          end
@@ -643,6 +646,11 @@ module JsDuck
          "In Windows this option is disabled.") do |count|
          @opts.processes = count.to_i
        end
        validator :processes do
          if @opts.processes.to_i < 0
            "Number of processes must be a positive number."
          end
        end

        attribute :cache, false
        option('--[no-]cache',
@@ -769,17 +777,17 @@ module JsDuck
          "Useful when developing the template files.") do |path|
          @opts.template_dir = canonical(path)
        end
        validator do
        validator :template_dir do
          if @opts.export
            # Don't check these things when exporting
          elsif !File.exists?(@opts.template_dir + "/extjs")
          elsif !@file.exists?(@opts.template_dir + "/extjs")
            [
              "Oh noes!  The template directory does not contain extjs/ directory :(",
              "Please copy ExtJS over to template/extjs or create symlink.",
              "For example:",
              "    $ cp -r /path/to/ext-4.0.0 " + @opts.template_dir + "/extjs",
            ]
          elsif !File.exists?(@opts.template_dir + "/resources/css")
          elsif !@file.exists?(@opts.template_dir + "/resources/css")
            [
              "Oh noes!  CSS files for custom ExtJS theme missing :(",
              "Please compile SASS files in template/resources/sass with compass.",
@@ -869,8 +877,8 @@ module JsDuck
        @opts.attribute(name, value)
      end

      def validator(&block)
        @opts.validator(&block)
      def validator(name, &block)
        @opts.validator(name, &block)
      end

      # Parses the given command line options
@@ -884,7 +892,7 @@ module JsDuck
      # Reads jsduck.json file in current directory
      def auto_detect_config_file
        fname = Dir.pwd + "/jsduck.json"
        if File.exists?(fname)
        if @file.exists?(fname)
          Logger.log("Auto-detected config file", fname)
          parse_options(read_json_config(fname))
        end
@@ -893,14 +901,14 @@ module JsDuck
      # Reads JSON configuration from file and returns an array of
      # config options that can be feeded into optparser.
      def read_json_config(filename)
        Options::Config.read(filename)
        @config.read(filename)
      end

      # When given string is a file, returns the contents of the file.
      # Otherwise returns the string unchanged.
      def maybe_file(str)
        path = canonical(str)
        if File.exists?(path)
        if @file.exists?(path)
          Util::IO.read(path)
        else
          str
@@ -913,7 +921,7 @@ module JsDuck
      # pathnames are converted to C:/foo/bar which ruby can work on
      # more easily.
      def canonical(path)
        File.expand_path(path, @working_dir)
        @file.expand_path(path, @working_dir)
      end

    end
+9 −5
Original line number Diff line number Diff line
@@ -11,7 +11,7 @@ module JsDuck
    # error.
    class Record
      def initialize
        @validators = []
        @validators = {}
      end

      # Defines accessor for an option,
@@ -38,14 +38,18 @@ module JsDuck
      # should return an error message string (or an array of string
      # for multi-line error message) otherwise nil, to signify
      # success.
      def validator(&block)
        @validators << block
      def validator(name, &block)
        @validators[name] = block
      end

      # Runs all the validators.  Returns an error message string from
      # the first failed validation or nil when everything is OK.
      def validate!
        @validators.each do |block|
      #
      # Alternatively runs just one validator by name. Used in testing.
      def validate!(name=nil)
        validators = name ? [@validators[name]] : @validators

        validators.each do |block|
          if err = block.call()
            return err
          end
+10 −1
Original line number Diff line number Diff line
@@ -15,7 +15,16 @@ module JsDuck
      end

      def method_missing(meth, *args, &block)
        @methods.has_key?(meth) ? @methods[meth] : self
        if @methods.has_key?(meth)
          value = @methods[meth]
          if value.respond_to?(:call)
            value.call(*args, &block)
          else
            value
          end
        else
          self
        end
      end

      def respond_to?(meth)

spec/options_spec.rb

0 → 100644
+459 −0
Original line number Diff line number Diff line
require "jsduck/util/null_object"
require "jsduck/options/parser"

describe JsDuck::Options::Parser do

  def mock_parse(methods, *argv)
    default_methods = {
      :dirname => Proc.new {|x| x },
      :expand_path => Proc.new {|x, pwd| x },
      :exists? => false,
    }
    file_class = JsDuck::Util::NullObject.new(default_methods.merge(methods))
    JsDuck::Options::Parser.new(file_class).parse(argv)
  end

  def parse(*argv)
    mock_parse({}, *argv)
  end

  describe :input_files do
    it "defaults to empty array" do
      parse("-o", "foo/").input_files.should == []
    end

    it "treats empty input files list as invalid" do
      parse("-o", "foo/").validate!(:input_files).should_not == nil
    end

    it "contains all non-option arguments" do
      parse("foo.js", "bar.js").input_files.should == ["foo.js", "bar.js"]
    end

    it "is populated by --builtin-classes" do
      parse("--builtin-classes").input_files[0].should =~ /js-classes$/
    end

    it "is valid when populated by --builtin-classes" do
      parse("--builtin-classes").validate!(:input_files).should == nil
    end
  end

  describe :output_dir do
    it "is set with --output option" do
      parse("--output", "foo/").output_dir.should == "foo/"
    end

    it "is set with -o option" do
      parse("-o", "foo/").output_dir.should == "foo/"
    end

    it "is set to :stdout with -" do
      parse("--output", "-").output_dir.should == :stdout
    end

    it "is invalid when :stdout but not export" do
      parse("--output", "-").validate!(:output_dir).should_not == nil
    end

    it "is valid when :stdout and export" do
      parse("--output", "-", "--export", "full").validate!(:output_dir).should == nil
    end

    it "is invalid when no output dir specified" do
      parse().validate!(:output_dir).should_not == nil
    end

    it "is valid when output dir exists and is a directory" do
      m = {:exists? => Proc.new {|f| f == "foo/"}, :directory? => true}
      mock_parse(m, "-o", "foo/").validate!(:output_dir).should == nil
    end

    it "is invalid when output dir is not a directory" do
      m = {:exists? => Proc.new {|f| f == "foo/"}, :directory? => false}
      mock_parse(m, "-o", "foo/").validate!(:output_dir).should_not == nil
    end

    it "is valid when parent dir of output dir exists" do
      m = {
        :exists? => Proc.new do |fname|
          case fname
          when "foo/"
            false
          when "parent/"
            true
          else
            false
          end
        end,
        :dirname => Proc.new do |fname|
          case fname
          when "foo/"
            "parent/"
          else
            fname
          end
        end
      }
      mock_parse(m, "-o", "foo/").validate!(:output_dir).should == nil
    end

    it "is invalid when parent dir of output dir is missing" do
      m = {:exists? => false}
      mock_parse(m, "-o", "foo/").validate!(:output_dir).should_not == nil
    end
  end

  describe :export do
    it "accepts --export=full" do
      opts = parse("--export", "full")
      opts.validate!(:export).should == nil
      opts.export.should == :full
    end

    it "accepts --export=examples" do
      opts = parse("--export", "examples")
      opts.validate!(:export).should == nil
      opts.export.should == :examples
    end

    it "doesn't accept --export=foo" do
      opts = parse("--export", "foo")
      opts.validate!(:export).should_not == nil
    end

    it "is valid when no export option specified" do
      opts = parse()
      opts.validate!(:export).should == nil
    end
  end

  describe :title do
    it "defaults to 'Documentation - JSDuck'" do
      opts = parse()
      opts.title.should == 'Documentation - JSDuck'
      opts.header.should == "<strong>Documentation</strong> JSDuck"
    end

    it "sets both title and header" do
      opts = parse("--title", "Docs - MyApp")
      opts.title.should == "Docs - MyApp"
      opts.header.should == "<strong>Docs</strong> MyApp"
    end
  end

  describe :guides_toc_level do
    it "defaults to 2" do
      parse().guides_toc_level.should == 2
    end

    it "gets converted to integer" do
      parse("--guides-toc-level", "6").guides_toc_level.should == 6
    end

    it "is valid when between 1..6" do
      opts = parse("--guides-toc-level", "1")
      opts.validate!(:guides_toc_level).should == nil
    end

    it "is invalid when not a number" do
      opts = parse("--guides-toc-level", "hello")
      opts.validate!(:guides_toc_level).should_not == nil
    end

    it "is invalid when larger then 6" do
      opts = parse("--guides-toc-level", "7")
      opts.validate!(:guides_toc_level).should_not == nil
    end
  end

  describe :imports do
    it "defaults to empty array" do
      parse().imports.should == []
    end

    it "expands into version and path components" do
      parse("--import", "1.0:/vers/1", "--import", "2.0:/vers/2").imports.should == [
        {:version => "1.0", :path => "/vers/1"},
        {:version => "2.0", :path => "/vers/2"},
      ]
    end

    it "expands pathless version number into just :version" do
      parse("--import", "3.0").imports.should == [
        {:version => "3.0"},
      ]
    end
  end

  describe :search do
    it "defaults to empty hash" do
      parse().search.should == {}
    end

    it "sets :url from --search-url" do
      parse("--search-url", "example.com").search[:url].should == "example.com"
    end

    it "sets :product and :version from --search-domain" do
      opts = parse("--search-domain", "Touch/2.0")
      opts.search[:product].should == "Touch"
      opts.search[:version].should == "2.0"
    end
  end

  describe :external_classes do
    it "contain Object and Array by default" do
      classes = parse().external_classes
      classes.should include("Object")
      classes.should include("Array")
    end

    it "can be used multiple times" do
      classes = parse("--external", "Foo", "--external", "Bar").external_classes
      classes.should include("Foo")
      classes.should include("Bar")
    end

    it "can be used with comma-separated list" do
      classes = parse("--external", "Foo,Bar").external_classes
      classes.should include("Foo")
      classes.should include("Bar")
    end
  end

  describe :ext_namespaces do
    it "defaults to nil" do
      parse().ext_namespaces.should == nil
    end

    it "can be used with comma-separated list" do
      parse("--ext-namespaces", "Foo,Bar").ext_namespaces.should == ["Foo", "Bar"]
    end

    it "can not be used multiple times" do
      parse("--ext-namespaces", "Foo", "--ext-namespaces", "Bar").ext_namespaces.should == ["Bar"]
    end
  end

  describe :ignore_html do
    it "defaults to empty hash" do
      parse().ignore_html.should == {}
    end

    it "can be used with comma-separated list" do
      html = parse("--ignore-html", "em,strong").ignore_html
      html.should include("em")
      html.should include("strong")
    end

    it "can be used multiple times" do
      html = parse("--ignore-html", "em", "--ignore-html", "strong").ignore_html
      html.should include("em")
      html.should include("strong")
    end
  end

  describe :processes do
    it "defaults to nil" do
      opts = parse()
      opts.validate!(:processes).should == nil
      opts.processes.should == nil
    end

    it "can be set to 0" do
      opts = parse("--processes", "0")
      opts.validate!(:processes).should == nil
      opts.processes.should == 0
    end

    it "can be set to any positive number" do
      opts = parse("--processes", "4")
      opts.validate!(:processes).should == nil
      opts.processes.should == 4
    end

    it "can not be set to a negative number" do
      opts = parse("--processes", "-6")
      opts.validate!(:processes).should_not == nil
    end
  end

  describe :template_dir do
    it "defaults to /template-min" do
      parse().template_dir.should =~ /template-min$/
    end

    it "is not validated when --export set" do
      opts = parse("--template", "foo", "--export", "full")
      opts.validate!(:template_dir).should == nil
    end

    it "is invalid when template dir has no /extjs dir" do
      m = {
        :exists? => false,
      }
      opts = mock_parse(m, "--template", "foo")
      opts.validate!(:template_dir).should_not == nil
    end

    it "is invalid when template dir has no /resources/css dir" do
      m = {
        :exists? => Proc.new {|fname| fname == "foo/extjs"},
      }
      opts = mock_parse(m, "--template", "foo")
      opts.validate!(:template_dir).should_not == nil
    end

    it "is valid when template dir contains both /extjs and /resouces/css dirs" do
      m = {
        :exists? => Proc.new {|fname| fname == "foo/extjs" || fname == "foo/resources/css" },
      }
      opts = mock_parse(m, "--template", "foo")
      opts.validate!(:template_dir).should == nil
    end
  end

  describe "--debug" do
    it "is equivalent of --template=template --template-links" do
      opts = parse("--debug")
      opts.template_dir.should == "template"
      opts.template_links.should == true
    end

    it "has a shorthand -d" do
      opts = parse("-d")
      opts.template_dir.should == "template"
      opts.template_links.should == true
    end
  end

  describe :warnings do
    it "default to empty array" do
      parse().warnings.should == []
    end

    it "are parsed with Warnings::Parser" do
      ws = parse("--warnings", "+foo,-bar").warnings
      ws.length.should == 2
      ws[0][:type].should == :foo
      ws[0][:enabled].should == true
      ws[1][:type].should == :bar
      ws[1][:enabled].should == false
    end
  end

  describe "--config" do
    it "interprets config options from config file" do
      file = JsDuck::Util::NullObject.new({
          :dirname => Proc.new {|x| x },
          :expand_path => Proc.new {|x, pwd| x },
          :exists? => Proc.new {|f| f == "conf.json" },
        })
      cfg = JsDuck::Util::NullObject.new({
          :read => ["-o", "foo", "file.js"]
        })

      opts = JsDuck::Options::Parser.new(file, cfg).parse(["--config", "conf.json"])
      opts.output_dir.should == "foo"
      opts.input_files.should == ["file.js"]
    end
  end

  # Boolean options
  {
    :seo => false,
    :tests => false,
    # :source => true, # TODO
    :ignore_global => false,
    :ext4_events => nil, # TODO
    :touch_examples_ui => false,
    :cache => false,
    :verbose => false,
    :warnings_exit_nonzero => false,
    :color => nil, # TODO
    :pretty_json => nil,
    :template_links => false,
  }.each do |attr, default|
    describe attr do
      it "defaults to false" do
        parse().send(attr).should == default
      end

      it "set to true when --#{attr} used" do
        parse("--#{attr.to_s.gsub(/_/, '-')}").send(attr).should == true
      end
    end
  end

  # Simple setters
  {
    :encoding => "--encoding",
    :footer => ["--footer", "Generated on {DATE} by {JSDUCK} {VERSION}."],
    :welcome => "--welcome",
    :guides => "--guides",
    :videos => "--videos",
    :examples => "--examples",
    :categories_path => "--categories",
    :new_since => "--new-since",
    :comments_url => "--comments-url",
    :comments_domain => "--comments-domain",
    :examples_base_url => "--examples-base-url",
    :link_tpl => ["--link", '<a href="#!/api/%c%-%m" rel="%c%-%m" class="docClass">%a</a>'],
    :img_tpl => ["--img", '<p><img src="%u" alt="%a" width="%w" height="%h"></p>'],
    :eg_iframe => "--eg-iframe",
    :cache_dir => "--cache-dir",
    :extjs_path => ["--extjs-path", "extjs/ext-all.js"],
    :local_storage_db => ["--local-storage-db", "docs"],
  }.each do |attr, (option, default)|
    describe attr do
      it "defaults to #{default.inspect}" do
        parse().send(attr).should == default
      end
      it "is set to given string value" do
        parse(option, "some string").send(attr).should == "some string"
      end
    end
  end

  # HTML and CSS options that get concatenated
  {
    :head_html => "--head-html",
    :body_html => "--body-html",
    :css => "--css",
    :message => "--message",
  }.each do |attr, option|
    describe attr do
      it "defaults to empty string" do
        parse().send(attr).should == ""
      end

      it "can be used multiple times" do
        parse(option, "Some ", option, "text").send(attr).should == "Some text"
      end
    end
  end

  # Multiple paths
  {
    :exclude => "--exclude",
    :images => "--images",
    :tags => "--tags",
  }.each do |attr, option|
    describe attr do
      it "defaults to empty array" do
        parse().send(attr).should == []
      end

      it "can be used multiple times" do
        parse(option, "foo", option, "bar").send(attr).should == ["foo", "bar"]
      end

      it "can be used with comma-separated list" do
        parse(option, "foo,bar").send(attr).should == ["foo", "bar"]
      end
    end
  end


end