diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 60b821c..5f1931d 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -109,6 +109,7 @@ Layout/SpaceInsideHashLiteralBraces: Exclude: - 'lib/evva/config.rb' - 'lib/evva/logger.rb' + - 'spec/evva_spec.rb' - 'spec/lib/evva/config_spec.rb' - 'spec/lib/evva/google_sheet_spec.rb' - 'spec/lib/evva/kotlin_generator_spec.rb' @@ -253,6 +254,8 @@ Style/HashSyntax: - 'Gemfile' - 'Rakefile' - 'lib/evva.rb' + - 'spec/evva_spec.rb' + - 'spec/platform_filter_spec.rb' - 'lib/evva/config.rb' - 'lib/evva/google_sheet.rb' - 'lib/evva/kotlin_generator.rb' @@ -359,6 +362,7 @@ Style/SymbolProc: # SupportedStylesForMultiline: comma, consistent_comma, no_comma Style/TrailingCommaInArrayLiteral: Exclude: + - 'spec/evva_spec.rb' - 'spec/lib/evva/google_sheet_spec.rb' - 'spec/lib/evva/kotlin_generator_spec.rb' - 'spec/lib/evva/swift_generator_spec.rb' @@ -370,6 +374,7 @@ Style/TrailingCommaInArrayLiteral: Style/TrailingCommaInHashLiteral: Exclude: - 'lib/evva/config.rb' + - 'spec/evva_spec.rb' - 'lib/evva/kotlin_generator.rb' - 'lib/evva/swift_generator.rb' - 'spec/lib/evva/config_spec.rb' diff --git a/Gemfile.lock b/Gemfile.lock index e879208..0a6d8e7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - evva (0.8.4) + evva (0.8.5) colorize (~> 1.0) csv (>= 3.0) safe_yaml (~> 1.0) diff --git a/README.md b/README.md index 24e3c53..7c42574 100644 --- a/README.md +++ b/README.md @@ -36,4 +36,43 @@ Evva automatically generates code for triggering events based on a Google Sheets destinations_file_name: /file/with/destinations special_enum_file_name: /file/with/special/enum/properties/ swift_public: false # optional; when true (iOS), generated Swift uses the public access modifier for generated extensions + filter_by_platforms: # optional; array of platforms to keep (e.g. [ios]) + - ios ``` + +## The events sheet + + The events sheet is read by column header, so column order does not matter and + unknown columns are ignored. + + | Column | Required | Meaning | + | --- | --- | --- | + | `Event Name` | yes | The event name | + | `Event Properties` | no | Comma separated `name:Type` pairs | + | `Event Destination` | no | Comma separated destinations | + | `Platform` | no | Which platforms track the event | + +### Platform + + `Platform` tags an event with the platforms it belongs to. Filtering is driven + by the `filter_by_platforms` key in `evva_config.yml`: only events matching one + of the listed platforms (or unrestricted events) are generated. When the key is + absent, every event is generated. + + Any value is accepted as a platform name (case insensitive, comma separated). + An empty cell means the event is unrestricted and will never be filtered out. + + The header is matched ignoring case and surrounding whitespace, so `platform` and + `Platform ` are both found. + + When no `Platform` column is found the run says so, because "every event was + generated" is otherwise indistinguishable from a header that failed to match: + + ``` + [INFO] No Platform column in the events sheet, every event will be generated for every platform + ``` + + Enums whose only referencing events were filtered out are pruned, unless a + people property also references them. + + People properties have no `Platform` column and are never filtered. diff --git a/lib/evva.rb b/lib/evva.rb index 713796c..4c61244 100644 --- a/lib/evva.rb +++ b/lib/evva.rb @@ -26,6 +26,7 @@ def run(options) config = Evva::Config.new(hash: YAML.safe_load(config_file)) bundle = analytics_data(config: config.data_source) filter_destinations!(bundle, config.exclude_destinations) + filter_platforms!(bundle, config.filter_by_platforms) case config.type.downcase when "android" generator = Evva::KotlinGenerator.new(config.package_name) @@ -102,6 +103,40 @@ def filter_destinations!(bundle, excluded) bundle[:people].each { |p| p.destinations.reject! { |d| excluded.include?(d) } } end + def filter_platforms!(bundle, platforms) + return if platforms.nil? || platforms.empty? + + normalized = platforms.map { |p| p.to_s.downcase } + + kept, filtered = bundle[:events].partition { |event| + normalized.any? { |p| event.supports_platform?(p) } + } + return if filtered.empty? + + bundle[:events] = kept + Logger.info("Filtered #{filtered.size} events (#{filtered.map(&:event_name).join(', ')})") + + # Only what the dropped events were keeping alive. An enum that had no + # reference before this run keeps its place on purpose: pruning those would + # change the output of sheets that have no Platform column at all. + orphaned = types_referenced_by(filtered) - types_referenced_by(kept, bundle[:people]) + pruned, remaining = bundle[:enums].partition { |enum| orphaned.include?(enum.enum_name) } + return if pruned.empty? + + bundle[:enums] = remaining + Logger.info("Pruned #{pruned.size} enums (#{pruned.map(&:enum_name).join(', ')})") + end + + # Every property type these events and people properties name, with the + # optional marker stripped. The caller compares against real enum names, so + # this never has to decide whether a type is an enum. People properties are + # never platform filtered but still hold references, so an enum kept alive only + # by one must survive. + def types_referenced_by(events, people = []) + types = events.flat_map { |event| event.properties.values } + people.map(&:type) + types.map { |type| type.to_s.chomp("?") } + end + def write_to_file(path, data) file_reader = Evva::FileReader.new if file = file_reader.open_file(path, "w", false) diff --git a/lib/evva/analytics_event.rb b/lib/evva/analytics_event.rb index afa59ca..b2e8fab 100644 --- a/lib/evva/analytics_event.rb +++ b/lib/evva/analytics_event.rb @@ -1,17 +1,25 @@ module Evva class AnalyticsEvent - attr_reader :event_name, :properties, :destinations + attr_reader :event_name, :properties, :destinations, :platforms - def initialize(event_name, properties, destinations) + def initialize(event_name, properties, destinations, platforms = nil) @event_name = event_name @properties = properties @destinations = destinations + @platforms = platforms + end + + def supports_platform?(platform) + return true if platforms.nil? + + platforms.include?(platform.to_s.downcase) end def ==(other) event_name == other.event_name && properties == other.properties && - destinations == other.destinations + destinations == other.destinations && + platforms == other.platforms end end end diff --git a/lib/evva/config.rb b/lib/evva/config.rb index 2f7888e..5ba4e42 100644 --- a/lib/evva/config.rb +++ b/lib/evva/config.rb @@ -68,6 +68,10 @@ def exclude_destinations @hash[:exclude_destinations] || [] end + def filter_by_platforms + @hash[:filter_by_platforms] + end + CONFIG_STRUCT = { type: Hash, elements: { @@ -83,7 +87,8 @@ def exclude_destinations destinations_file_name: { type: String }, package_name: { type: String }, swift_public: { type: Object, optional: true }, - exclude_destinations: { type: Array, optional: true } + exclude_destinations: { type: Array, optional: true }, + filter_by_platforms: { type: Array, optional: true } } }.freeze diff --git a/lib/evva/google_sheet.rb b/lib/evva/google_sheet.rb index 64aabff..89a7464 100644 --- a/lib/evva/google_sheet.rb +++ b/lib/evva/google_sheet.rb @@ -6,6 +6,7 @@ class GoogleSheet EVENT_NAME = "Event Name" EVENT_PROPERTIES = "Event Properties" EVENT_DESTINATION = "Event Destination" + EVENT_PLATFORM = "Platform" PROPERTY_NAME = "Property Name" PROPERTY_TYPE = "Property Type" @@ -26,11 +27,17 @@ def events get_csv(@events_url) end - @events ||= @events_csv.map do |row| - event_name = row[EVENT_NAME] - properties = hash_parser(row[EVENT_PROPERTIES]) - destinations = row[EVENT_DESTINATION]&.split(",") - Evva::AnalyticsEvent.new(event_name, properties, destinations || []) + @events ||= begin + platform_header = header_matching(@events_csv, EVENT_PLATFORM) + Logger.info("No #{EVENT_PLATFORM} column in the events sheet, every event will be generated for every platform") if platform_header.nil? + + @events_csv.map do |row| + event_name = row[EVENT_NAME] + properties = hash_parser(row[EVENT_PROPERTIES]) + destinations = row[EVENT_DESTINATION]&.split(",") + platforms = platform_parser(platform_header && row[platform_header]) + Evva::AnalyticsEvent.new(event_name, properties, destinations || [], platforms) + end end end @@ -96,6 +103,21 @@ def get(url, max_redirects = 1) response.body end + # Matched ignoring case and surrounding whitespace, because a header of + # "platform" or "Platform " means what it plainly means. + def header_matching(csv, name) + csv.headers.compact.find { |header| header.to_s.strip.casecmp?(name) } + end + + # nil (no Platform column or empty cell) means unrestricted (never filtered out). + # Concrete platform tokens are stored lowercased and deduplicated. + def platform_parser(platform_list) + tokens = platform_list.to_s.split(",").map(&:strip).reject(&:empty?) + return nil if tokens.empty? + + tokens.map(&:downcase).uniq + end + def hash_parser(property_array) h = {} unless property_array.nil? || property_array.empty? diff --git a/spec/evva_spec.rb b/spec/evva_spec.rb index 6291c1c..586cd06 100644 --- a/spec/evva_spec.rb +++ b/spec/evva_spec.rb @@ -57,6 +57,189 @@ end end + describe ".filter_platforms!" do + def event(name, platforms, properties = {}) + Evva::AnalyticsEvent.new(name, properties, ["firebase"], platforms) + end + + let(:unrestricted) { event("unrestricted", nil) } + let(:ios_only) { event("ios_only", ["ios"], { from_screen: "IosOnlyEnum" }) } + let(:android_only) { event("android_only", ["android"], { from_screen: "AndroidOnlyEnum" }) } + + let(:events) { [unrestricted, ios_only, android_only] } + + let(:enums) do + [ + Evva::AnalyticsEnum.new("IosOnlyEnum", ["a"]), + Evva::AnalyticsEnum.new("AndroidOnlyEnum", ["b"]), + Evva::AnalyticsEnum.new("NeverReferenced", ["c"]), + ] + end + + let(:people) { [Evva::AnalyticsProperty.new("total_friends", "Int", [])] } + + let(:bundle) do + { destinations: ["firebase"], events: events, people: people, enums: enums } + end + + def enum_names + bundle[:enums].map(&:enum_name) + end + + def event_names + bundle[:events].map(&:event_name) + end + + context "when filtering for iOS" do + before { Evva.filter_platforms!(bundle, ["ios"]) } + + it "keeps unrestricted events" do + expect(event_names).to include("unrestricted") + end + + it "keeps iOS events" do + expect(event_names).to include("ios_only") + end + + it "drops Android events" do + expect(event_names).not_to include("android_only") + end + end + + context "when filtering for Android" do + before { Evva.filter_platforms!(bundle, ["android"]) } + + it "keeps unrestricted events" do + expect(event_names).to include("unrestricted") + end + + it "keeps Android events" do + expect(event_names).to include("android_only") + end + + it "drops iOS events" do + expect(event_names).not_to include("ios_only") + end + end + + context "when filtering for multiple platforms" do + before { Evva.filter_platforms!(bundle, ["ios", "android"]) } + + it "keeps events matching any of them" do + expect(event_names).to eq(["unrestricted", "ios_only", "android_only"]) + end + end + + context "when filter_by_platforms is nil" do + before { Evva.filter_platforms!(bundle, nil) } + + it "does not filter anything" do + expect(event_names).to eq(["unrestricted", "ios_only", "android_only"]) + end + end + + context "when filter_by_platforms is empty" do + before { Evva.filter_platforms!(bundle, []) } + + it "does not filter anything" do + expect(event_names).to eq(["unrestricted", "ios_only", "android_only"]) + end + end + + it "accounts for every event it was given" do + events_in = bundle[:events].size + Evva.filter_platforms!(bundle, ["ios"]) + events_out = bundle[:events].size + events_filtered = events.count { |e| !e.supports_platform?("ios") } + + expect(events_in).to eq(events_out + events_filtered) + end + + context "when no event is filtered out" do + let(:events) { [unrestricted] } + + it "leaves the events untouched" do + expect { Evva.filter_platforms!(bundle, ["ios"]) }.not_to change { bundle[:events] } + end + + it "leaves the enums untouched, orphans included" do + expect { Evva.filter_platforms!(bundle, ["ios"]) }.not_to change { enum_names } + end + + it "logs nothing" do + expect { + Evva.filter_platforms!(bundle, ["ios"]) + }.to not_change { Evva::Logger.summary[:info] } + end + end + + describe "enum pruning" do + it "prunes an enum whose only event was filtered out" do + Evva.filter_platforms!(bundle, ["ios"]) + + expect(enum_names).not_to include("AndroidOnlyEnum") + end + + it "keeps an enum still referenced by a surviving event" do + Evva.filter_platforms!(bundle, ["ios"]) + + expect(enum_names).to include("IosOnlyEnum") + end + + it "keeps an enum that was already unreferenced before filtering" do + Evva.filter_platforms!(bundle, ["ios"]) + + expect(enum_names).to include("NeverReferenced") + end + + context "when a filtered event's enum is also referenced by a people property" do + let(:people) do + [Evva::AnalyticsProperty.new("wearable_platform", "AndroidOnlyEnum", [])] + end + + it "keeps the enum" do + Evva.filter_platforms!(bundle, ["ios"]) + + expect(enum_names).to include("AndroidOnlyEnum") + end + end + + context "when the reference is optional" do + let(:android_only) { event("android_only", ["android"], { from_screen: "AndroidOnlyEnum?" }) } + let(:ios_only) { event("ios_only", ["ios"], { from_screen: "IosOnlyEnum?" }) } + + it "prunes the filtered platform's enum" do + Evva.filter_platforms!(bundle, ["ios"]) + + expect(enum_names).not_to include("AndroidOnlyEnum") + end + + it "keeps the surviving platform's enum" do + Evva.filter_platforms!(bundle, ["ios"]) + + expect(enum_names).to include("IosOnlyEnum") + end + end + end + + describe "logging" do + subject(:messages) do + logged = [] + allow(Evva::Logger).to receive(:info) { |msg| logged << msg } + Evva.filter_platforms!(bundle, ["ios"]) + logged + end + + it "names the filtered events" do + expect(messages).to include("Filtered 1 events (android_only)") + end + + it "names the pruned enums" do + expect(messages).to include("Pruned 1 enums (AndroidOnlyEnum)") + end + end + end + context "when generic.yml does not exist locally" do let(:error) { "Could not open yml file" } before do diff --git a/spec/fixtures/sample_public_enums_with_platform.csv b/spec/fixtures/sample_public_enums_with_platform.csv new file mode 100644 index 0000000..e4bdae9 --- /dev/null +++ b/spec/fixtures/sample_public_enums_with_platform.csv @@ -0,0 +1,6 @@ +Enum Name,Possible Values +PageViewSourceScreen,"course_discovery,synced_courses,nearby,deal" +IosOnlyEnum,"a,b" +AndroidOnlyEnum,"c,d" +SharedEnum,"e,f" +WearableAppPlatform,"wear_os,watch_os" diff --git a/spec/fixtures/sample_public_events_with_platform.csv b/spec/fixtures/sample_public_events_with_platform.csv new file mode 100644 index 0000000..8dd1896 --- /dev/null +++ b/spec/fixtures/sample_public_events_with_platform.csv @@ -0,0 +1,7 @@ +Event Name,Platform,Event Properties,Added in,Event Destination,Description +cp_page_view,,"course_id:Long,course_name:String",1.0,"firebase,custom destination",Empty cell means every platform +ios_only_event,iOS,from_screen:IosOnlyEnum,1.1,firebase,Only tracked on iOS +android_only_event,Android,from_screen:AndroidOnlyEnum,1.1,firebase,Only tracked on Android +multi_platform_event,"iOS, Android",source:SharedEnum,1.2,firebase,Both platforms spelled out as a list +mixed_case_event,IOS,,1.2,firebase,Casing is normalised +wear_sync_event,iOS,platform:WearableAppPlatform,1.3,firebase,Enum also referenced by a people property diff --git a/spec/lib/evva/config_spec.rb b/spec/lib/evva/config_spec.rb index 0c5ce43..9d95d45 100644 --- a/spec/lib/evva/config_spec.rb +++ b/spec/lib/evva/config_spec.rb @@ -65,6 +65,24 @@ end end + describe "#filter_by_platforms" do + context "when not set" do + its(:filter_by_platforms) { should be_nil } + end + + context "when set to an array" do + before { hash[:filter_by_platforms] = ["ios"] } + + its(:filter_by_platforms) { should eq(["ios"]) } + end + + context "when not an array" do + before { hash[:filter_by_platforms] = "ios" } + + it { expect { config }.to raise_error(ArgumentError, /Expected Array, got String/) } + end + end + describe "#swift_public?" do context "when swift_public is true" do before { hash[:swift_public] = true } diff --git a/spec/lib/evva/google_sheet_spec.rb b/spec/lib/evva/google_sheet_spec.rb index 820e21a..4277e43 100644 --- a/spec/lib/evva/google_sheet_spec.rb +++ b/spec/lib/evva/google_sheet_spec.rb @@ -23,14 +23,120 @@ it "returns an array with the corresponding events" do expected = [ - Evva::AnalyticsEvent.new("cp_page_view", { course_id: "Long", course_name: "String" }, ["firebase", "custom destination"]), - Evva::AnalyticsEvent.new("nav_feed_tap", {}, []), - Evva::AnalyticsEvent.new("cp_view_scorecard", { course_id: "Long", course_name: "String" }, ["custom destination"]), - Evva::AnalyticsEvent.new("side_game_delete", { fromScreen: "SideGameFromScreen", round_group_creation_token: "String" }, ["firebase"]), + Evva::AnalyticsEvent.new("cp_page_view", { course_id: "Long", course_name: "String" }, ["firebase", "custom destination"], nil), + Evva::AnalyticsEvent.new("nav_feed_tap", {}, [], nil), + Evva::AnalyticsEvent.new("cp_view_scorecard", { course_id: "Long", course_name: "String" }, ["custom destination"], nil), + Evva::AnalyticsEvent.new("side_game_delete", { fromScreen: "SideGameFromScreen", round_group_creation_token: "String" }, ["firebase"], nil), ] expect(events).to eq(expected) end + context "when the sheet has no Platform column" do + it "leaves platforms as nil (unrestricted)" do + expect(events.map(&:platforms)).to all(be_nil) + end + end + + context "when the sheet has a Platform column" do + let(:events_file) { File.read("spec/fixtures/sample_public_events_with_platform.csv") } + + def platforms_for(event_name) + events.find { |event| event.event_name == event_name }.platforms + end + + it do + expect { events }.not_to raise_error + end + + it "reads the column by header name, whatever its position" do + expect(events.map(&:event_name)).to eq(%w[ + cp_page_view + ios_only_event + android_only_event + multi_platform_event + mixed_case_event + wear_sync_event + ]) + end + + it "treats an empty cell as unrestricted" do + expect(platforms_for("cp_page_view")).to be_nil + end + + it "reads a single platform" do + expect(platforms_for("ios_only_event")).to eq(["ios"]) + expect(platforms_for("android_only_event")).to eq(["android"]) + end + + it "reads a comma separated list of platforms" do + expect(platforms_for("multi_platform_event")).to eq(["ios", "android"]) + end + + it "normalises casing" do + expect(platforms_for("mixed_case_event")).to eq(["ios"]) + end + end + + ["platform", "Platform ", " Platform", "PLATFORM", "pLaTfOrM"].each do |header| + context "when the Platform header is written as #{header.inspect}" do + let(:events_file) { %(Event Name,"#{header}"\nsome_event,Android\n) } + + it "still finds the column" do + expect(events.first.platforms).to eq(["android"]) + end + end + end + + context "when there is no Platform column" do + let(:events_file) { "Event Name,Event Destination\nsome_event,firebase\n" } + + it "says so, so that a mistyped header is not mistaken for a filter that ran" do + logged = [] + allow(Evva::Logger).to receive(:info) { |msg| logged << msg } + + events + + expect(logged).to include(/No Platform column/) + end + end + + context "when the Platform column is present" do + let(:events_file) { "Event Name,Platform\nsome_event,iOS\n" } + + it "does not report a missing column" do + logged = [] + allow(Evva::Logger).to receive(:info) { |msg| logged << msg } + + events + + expect(logged).not_to include(/No Platform column/) + end + end + + context "when a platform cell is written in another casing" do + let(:events_file) { "Event Name,Platform\nsome_event, AnDrOiD \n" } + + it "normalises it" do + expect(events.first.platforms).to eq(["android"]) + end + end + + context "when a platform cell repeats a platform" do + let(:events_file) { "Event Name,Platform\nsome_event,\"ios,iOS\"\n" } + + it "does not duplicate it" do + expect(events.first.platforms).to eq(["ios"]) + end + end + + context "when a platform cell has an unknown token" do + let(:events_file) { "Event Name,Platform\nsome_event,Windows\n" } + + it "stores it as-is (lowercased)" do + expect(events.first.platforms).to eq(["windows"]) + end + end + context "when given an inexistent sheet" do before { stub_request(:get, events_sheet).to_return(status: 400, body: "Not Found", headers: {}) } diff --git a/spec/platform_filter_spec.rb b/spec/platform_filter_spec.rb new file mode 100644 index 0000000..7c38eb5 --- /dev/null +++ b/spec/platform_filter_spec.rb @@ -0,0 +1,147 @@ +describe "filtering by platform end to end" do + let(:events_url) { "https://wtvr1" } + let(:people_url) { "https://wtvr2" } + let(:enum_url) { "https://wtvr3" } + + let(:events_fixture) { "sample_public_events_with_platform.csv" } + let(:enums_fixture) { "sample_public_enums_with_platform.csv" } + + before do + stub_request(:get, events_url).to_return(status: 200, body: fixture(events_fixture)) + stub_request(:get, people_url).to_return(status: 200, body: fixture("sample_public_people_properties.csv")) + stub_request(:get, enum_url).to_return(status: 200, body: fixture(enums_fixture)) + end + + def fixture(name) + File.read("spec/fixtures/#{name}") + end + + def unfiltered_bundle + source = { + type: "google_sheet", + events_url: events_url, + people_properties_url: people_url, + enum_classes_url: enum_url + } + Evva.analytics_data(config: source) + end + + def bundle_for(platforms) + bundle = unfiltered_bundle + Evva.filter_platforms!(bundle, platforms) + bundle + end + + def logged_while + logged = [] + allow(Evva::Logger).to receive(:info) { |msg| logged << msg } + yield + logged + end + + describe "for iOS" do + let(:bundle) { bundle_for(["ios"]) } + + it "keeps the iOS and unrestricted events" do + expect(bundle[:events].map(&:event_name)).to eq(%w[ + cp_page_view + ios_only_event + multi_platform_event + mixed_case_event + wear_sync_event + ]) + end + + it "accounts for every event it was given" do + filtered = logged_while { bundle }.grep(/^Filtered/).first.to_s[/\((.*)\)/, 1].to_s.split(", ") + + expect(bundle[:events].size + filtered.size).to eq(unfiltered_bundle[:events].size) + end + + it "prunes the Android only enum and keeps the rest" do + expect(bundle[:enums].map(&:enum_name)).to eq(%w[ + PageViewSourceScreen + IosOnlyEnum + SharedEnum + WearableAppPlatform + ]) + end + end + + describe "for Android" do + let(:bundle) { bundle_for(["android"]) } + + it "keeps the Android and unrestricted events" do + expect(bundle[:events].map(&:event_name)).to eq(%w[ + cp_page_view + android_only_event + multi_platform_event + ]) + end + + it "prunes the iOS only enum" do + expect(bundle[:enums].map(&:enum_name)).not_to include("IosOnlyEnum") + end + + # wear_sync_event is iOS only and gets filtered here, but the + # wearable_platform people property still references the enum. + it "keeps an enum held only by a people property" do + expect(bundle[:enums].map(&:enum_name)).to include("WearableAppPlatform") + end + + it "keeps the enum no event ever referenced" do + expect(bundle[:enums].map(&:enum_name)).to include("PageViewSourceScreen") + end + end + + describe "for multiple platforms" do + let(:bundle) { bundle_for(["ios", "android"]) } + + it "keeps events matching any of the platforms" do + expect(bundle[:events].map(&:event_name)).to eq(%w[ + cp_page_view + ios_only_event + android_only_event + multi_platform_event + mixed_case_event + wear_sync_event + ]) + end + end + + # What protects the sheets that predate the column. + # The generators are pure functions of the bundle and are untouched here, so a + # bundle the filter left alone generates exactly what it generated before. + describe "a sheet with no Platform column" do + let(:events_fixture) { "sample_public_events.csv" } + let(:enums_fixture) { "sample_public_enums.csv" } + + [["ios"], ["android"]].each do |platforms| + it "leaves the #{platforms} bundle untouched" do + before_filtering = unfiltered_bundle + after_filtering = bundle_for(platforms) + + expect(after_filtering[:events].map(&:event_name)).to eq(before_filtering[:events].map(&:event_name)) + expect(after_filtering[:enums].map(&:enum_name)).to eq(before_filtering[:enums].map(&:enum_name)) + expect(after_filtering[:people].map(&:property_name)).to eq(before_filtering[:people].map(&:property_name)) + end + end + + it "keeps enums that no event references" do + expect(bundle_for(["ios"])[:enums].map(&:enum_name)).to eq(%w[PageViewSourceScreen PremiumClickBuy]) + end + + it "reports no filtering" do + expect(logged_while { bundle_for(["ios"]) }.grep(/filtered|pruned/)).to be_empty + end + end + + describe "when filter_by_platforms is nil" do + it "leaves the bundle untouched" do + before_filtering = unfiltered_bundle + after_filtering = bundle_for(nil) + + expect(after_filtering[:events].map(&:event_name)).to eq(before_filtering[:events].map(&:event_name)) + end + end +end