Back to Repositories

Testing Xcode Project Synchronization Framework in Synx

This comprehensive test suite validates the Synx::Project class functionality, focusing on project synchronization and file structure management in Ruby. The tests ensure proper handling of Xcode project group structures, file organization, and build setting configurations.

Test Coverage Overview

The test suite covers project synchronization, file structure verification, and group management functionality. Key areas include:

  • File structure synchronization and validation
  • Xcode group structure management
  • Build settings path updates
  • Group exclusion handling
  • Project pathname resolution

Implementation Analysis

The testing approach utilizes RSpec’s describe/it blocks with nested contexts to organize test scenarios. Implementation includes:

  • Helper methods for structure verification
  • YAML-based expected structure definitions
  • Before hooks for test setup
  • Extensive path manipulation testing

Technical Details

Testing infrastructure includes:

  • RSpec as the testing framework
  • FileUtils for file system operations
  • Pathname for path manipulation
  • YAML for test data structure
  • Custom Hash extensions for data manipulation

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Modular test organization
  • Comprehensive edge case coverage
  • Clear test descriptions
  • Reusable helper methods
  • Isolated test scenarios

venmo/synx

spec/synx/project_spec.rb

            
require 'spec_helper'
require 'fileutils'
require 'pathname'
require 'yaml'

class Hash
  # Return a hash that includes everything but the given keys. This is useful for
  # limiting a set of parameters to everything but a few known toggles:
  #
  #   @person.update_attributes(params[:person].except(:admin))
  #
  # If the receiver responds to +convert_key+, the method is called on each of the
  # arguments. This allows +except+ to play nice with hashes with indifferent access
  # for instance:
  #
  #   {:a => 1}.with_indifferent_access.except(:a)  # => {}
  #   {:a => 1}.with_indifferent_access.except("a") # => {}
  #
  def except(*keys)
    dup.except!(*keys)
  end

  # Replaces the hash without the given keys.
  def except!(*keys)
    keys.each { |key| delete(key) }
    self
  end
end

describe Synx::Project do

  describe "#sync" do

    def verify_group_structure(group, expected_structure)
      expected_structure.each_with_index do |(object_name, object_children), index|
        failure_message = "expected group `#{group.basename}` to have child `#{object_name}`"
        object = group.children[index]
        expect(object.basename).to eq(object_name)
        expect(group).to_not be_nil, failure_message
        next if ["Products", "Frameworks"].include?(object.display_name)

        if object.instance_of?(Xcodeproj::Project::Object::PBXGroup)
          object_children ||= {}
          found_children = object.children.map(&:basename)
          missing_children_in_group = object_children.keys - found_children
          extra_children_in_group = found_children - object_children.keys
          failure_message = "In group #{object.hierarchy_path}:"

          unless missing_children_in_group.empty?
            failure_message += "
  Expected to find children: #{missing_children_in_group.join(", ")}"
          end

          unless extra_children_in_group.empty?
            failure_message += "
  Did not expect to find children: #{extra_children_in_group.join(", ")}"
          end
          failure_message = "Expected #{object_name} to have #{object_children.count} children, found #{object.children.count}"
          expect(object_children.count).to eq(object.children.count), failure_message
          verify_group_structure(object, object_children) if object_children.count > 0
        end
      end
    end

    def verify_file_structure(dir_pathname, expected_structure)
      expected_structure.each do |entry_name, entry_entries|
        entry_pathname = dir_pathname + entry_name
        expect(File.exist?(entry_pathname) || Dir.exists?(entry_pathname)).to be(true), "Expected #{entry_pathname} to exist"

        if File.directory?(entry_pathname)
          entry_entries ||= {}
          found_entries = entry_pathname.entries.reject { |e| [".", ".."].include?(e.to_s) }.map(&:to_s)
          missing_entries_on_file_system = entry_entries.keys - found_entries
          extra_entries_on_file_system = found_entries - entry_entries.keys
          failure_message = "In #{entry_pathname}:"

          unless missing_entries_on_file_system.empty?
            failure_message += "
  Expected to find entries: #{missing_entries_on_file_system.join(", ")}"
          end

          unless extra_entries_on_file_system.empty?
            failure_message += "
  Did not expect to find entries: #{extra_entries_on_file_system.join(", ")}"
          end

          expect(missing_entries_on_file_system.count + extra_entries_on_file_system.count).to be(0), failure_message
          verify_file_structure(entry_pathname, entry_entries) if entry_entries.count > 0
        end
      end
    end

    def expected_file_structure
      YAML::load_file(File.expand_path("../expected_file_structure.yml", __FILE__))
    end

    def expected_group_structure
      YAML::load_file(File.expand_path("../expected_group_structure.yml", __FILE__))
    end

    describe "with no additional options" do

      before(:all) do
        DUMMY_SYNX_TEST_PROJECT.sync(:output => StringIO.new)
      end

      it "should have the correct physical file structure" do
        verify_file_structure(Pathname(DUMMY_SYNX_TEST_PROJECT_PATH).parent, expected_file_structure)
      end

      it "should not have modified the Xcode group structure, except for fixing double file references" do
        verify_group_structure(DUMMY_SYNX_TEST_PROJECT.main_group, expected_group_structure)
      end

      it "should have updated the pch and info.plist build setting paths" do
        # dummy target
        DUMMY_SYNX_TEST_PROJECT.targets.first.each_build_settings do |bs|
          expect(bs["GCC_PREFIX_HEADER"]).to eq("dummy/Supporting Files/dummy-Prefix.pch")
          expect(bs["INFOPLIST_FILE"]).to be_nil
        end

        # dummyTests target
        DUMMY_SYNX_TEST_PROJECT.targets[1].each_build_settings do |bs|
          expect(bs["GCC_PREFIX_HEADER"]).to eq("dummyTests/Supporting Files/dummyTests-Prefix.pch")
          expect(bs["INFOPLIST_FILE"]).to eq("dummyTests/Supporting Files/dummyTests-Info.plist")
        end
      end
    end

    describe "with the prune option toggled" do

      before(:all) do
        DUMMY_SYNX_TEST_PROJECT.sync(:prune => true, :output => StringIO.new)
      end

      it "should remove unreferenced images and source files if the prune option is toggled" do
        expected_file_structure_with_removals = expected_file_structure
        expected_file_structure_with_removals["dummy"].except!("image-not-in-xcodeproj.png")
        expected_file_structure_with_removals["dummy"].except!("FileNotInXcodeProj.h")
        expected_file_structure_with_removals["dummy"]["AlreadySynced"].except!("FolderNotInXcodeProj")
        verify_file_structure(Pathname(DUMMY_SYNX_TEST_PROJECT_PATH).parent, expected_file_structure_with_removals)
      end

      it "should not have modified the Xcode group structure, except for fixing double file references" do
        verify_group_structure(DUMMY_SYNX_TEST_PROJECT.main_group, expected_group_structure)
      end
    end

    describe "with the no_default_exclusions option toggled" do

      before(:all) do
        DUMMY_SYNX_TEST_PROJECT.sync(:no_default_exclusions => true, :output => StringIO.new)
      end

      it "should have an empty array for default exclusions" do
        expect(DUMMY_SYNX_TEST_PROJECT.group_exclusions.count).to eq(0)
      end
    end

    describe "with group_exclusions provided as options" do

      before(:all) do
        DUMMY_SYNX_TEST_PROJECT.sync(:group_exclusions => %W(/dummy /dummy/SuchGroup/VeryChildGroup), :output => StringIO.new)
      end

      it "should add the group exclusions to the array" do
        expect(DUMMY_SYNX_TEST_PROJECT.group_exclusions.sort).to eq(%W(/Libraries /Products /Frameworks /Pods /dummy /dummy/SuchGroup/VeryChildGroup).sort)
      end
    end

  end

  describe "group_exclusions=" do

    it "should raise an IndexError if any of the groups do not exist" do
      expect { DUMMY_SYNX_TEST_PROJECT.group_exclusions = %W(/dummy /dummy/DoesntExist) }.to raise_error(IndexError)
    end

    it "should be fine if the groups all exist" do
      group_exclusions = %W(/dummy /dummy/GroupThatDoubleReferencesFile /dummy/SuchGroup/VeryChildGroup)
      DUMMY_SYNX_TEST_PROJECT.group_exclusions = group_exclusions

      expect(DUMMY_SYNX_TEST_PROJECT.group_exclusions).to eq(group_exclusions)
    end

    it "should be forgiving about missing '/' at beginning of group paths" do
      group_exclusions = %W(dummy dummy/GroupThatDoubleReferencesFile dummy/SuchGroup/VeryChildGroup)
      DUMMY_SYNX_TEST_PROJECT.group_exclusions = group_exclusions

      expected = %W(/dummy /dummy/GroupThatDoubleReferencesFile /dummy/SuchGroup/VeryChildGroup)
      expect(DUMMY_SYNX_TEST_PROJECT.group_exclusions).to eq(expected)
    end
  end

  describe "#root_pathname" do

    it "should return the pathname to the directory that the .pbxproj file is inside" do
      expected = Pathname(File.join(File.dirname(__FILE__), '..', 'test_dummy'))
      DUMMY_SYNX_TEST_PROJECT.send(:root_pathname).realpath.should eq(expected.realpath)
    end
  end

  describe "#work_root_pathname" do

    before(:each) { DUMMY_SYNX_TEST_PROJECT.instance_variable_set("@work_root_pathname", nil) }

    it "should return the pathname to the directory synxchronize will do its work in" do
      expected = Pathname(Synx::Project.const_get(:SYNXRONIZE_DIR)) + "test_dummy"
      DUMMY_SYNX_TEST_PROJECT.send(:work_root_pathname).realpath.should eq(expected.realpath)
    end

    it "should start fresh by removing any existing directory at work_root_pathname" do
      Pathname.any_instance.stub(:exist?).and_return(true)
      expect(FileUtils).to receive(:rm_rf)

      DUMMY_SYNX_TEST_PROJECT.send(:work_root_pathname)
    end

    it "should create a directory at work_root_pathname" do
      expect_any_instance_of(Pathname).to receive(:mkpath)
      DUMMY_SYNX_TEST_PROJECT.send(:work_root_pathname)
    end

    it "should be an idempotent operation but return the same value through memoization" do
      pathname = DUMMY_SYNX_TEST_PROJECT.send(:work_root_pathname)
      expect(FileUtils).to_not receive(:rm_rf)
      expect_any_instance_of(Pathname).to_not receive(:exist?)
      expect_any_instance_of(Pathname).to_not receive(:mkpath)
      expect(DUMMY_SYNX_TEST_PROJECT.send(:work_root_pathname)).to be(pathname)
    end
  end

  describe "#pathname_to_work_pathname" do

    it "should return the path in work_root_pathname that is relatively equivalent to root_pathname" do
      pathname = DUMMY_SYNX_TEST_PROJECT.send(:root_pathname) + "some" + "path" + "to" + "thing"

      value = DUMMY_SYNX_TEST_PROJECT.send(:pathname_to_work_pathname, pathname)
      expected = DUMMY_SYNX_TEST_PROJECT.send(:work_root_pathname) + "some" + "path" + "to" + "thing"

      expect(value).to eq(expected)
    end
  end
end