/* * Copyright 2015 The Closure Compiler Authors. * * Licensed 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. */ package com.google.javascript.jscomp; import static com.google.common.truth.Truth.assertThat; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import java.util.Collections; import java.util.Random; import java.util.Set; import junit.framework.TestCase; public final class RandomNameGeneratorTest extends TestCase { private static String[] generate(RandomNameGenerator ng, int count) throws Exception { String[] result = new String[count]; for (int i = 0; i < count; i++) { result[i] = ng.generateNextName(); } return result; } public static void testConstructorInvalidPrefixes() throws Exception { Random random = new Random(0); try { new RandomNameGenerator(Collections.<String>emptySet(), "123abc", null, random); fail("Constructor should throw exception when the first char of prefix " + "is invalid"); } catch (IllegalArgumentException ex) { // The error messages should contain meaningful information. assertThat(ex.getMessage()).contains("W, X, Y, Z, $"); } try { new RandomNameGenerator(Collections.<String>emptySet(), "abc%", null, random); fail("Constructor should throw exception when one of prefix characters " + "is invalid"); } catch (IllegalArgumentException ex) { assertThat(ex.getMessage()).contains("W, X, Y, Z, _, 0, 1"); } } public static void testGenerate() throws Exception { // Since there's a hash function involved, there's not much point in // mocking Random to get nicer values. Instead, let's just try to // verify the sanity of the results. Random random = new Random(0); Set<String> reservedNames = ImmutableSet.of(); String prefix = "prefix"; int prefixLen = prefix.length(); // Add a prefix to avoid dropping JavaScript keywords. RandomNameGenerator ng = new RandomNameGenerator( reservedNames, prefix, null, random); // Generate all 1- and 2-character names. // alphabet length, 1st digit int len1 = RandomNameGenerator.NONFIRST_CHAR.length; // alphabet length, 2nd digit int len2 = RandomNameGenerator.NONFIRST_CHAR.length; // len1 == len2 because we have a prefix int count = len1 * (1 + len2); String[] result = generate(ng, count); Set<String> resultSet = Sets.newHashSet(result); // We got as many names as we asked for, and all are different. assertThat(resultSet).hasSize(count); // First come names with length 1, then 2. No names are longer. for (int i = 0; i < len1; ++i) { assertThat(result[i].length()).isEqualTo(prefixLen + 1); } for (int i = len1; i < count; ++i) { assertThat(result[i].length()).isEqualTo(prefixLen + 2); } // We don't just have an alphabet for the first character and one for the // second, i.e. if we just had the valid characters A,B,C,D,E and the // shuffle was such that we started with C,D,A,B,E as names, the following // names would not just be CC,DC,AC,BC,EC,CD,DD,AD,BD,ED,... // Of course it could randomly happen some time, which is why we check // for all occurrences. // Verify that we don't get C,D,A,B,E,_C,_C,_C,_C,_C,_D,_D,_D,_D,_D_,... int countPass = 0; int countTest = 0; for (int i1 = 0; i1 < len1; ++i1) { for (int i2 = 0; i2 < len2; ++i2, ++countTest) { if (result[i1].charAt(prefixLen) != result[len1 + i1 * len2 + i2].charAt(prefixLen + 1)) { countPass++; } } } assertThat(100.0 * countPass / countTest).isGreaterThan(80.0); // arbitrary threshold // Names are not sorted (some might be, by chance) countPass = 0; countTest = 0; for (int i = 0; i < count - 1; ++i) { if (result[i].compareTo(result[i + 1]) > 0) { countPass++; } } assertThat(100.0 * countPass / countTest).isGreaterThan(25.0); // arbitrary threshold } public static void testFirstCharAlphabet() throws Exception { Random random = new Random(0); Set<String> reservedNames = ImmutableSet.of(); RandomNameGenerator ng = new RandomNameGenerator( reservedNames, "", null, random); // Generate all 1- and 2-character names. int len1 = RandomNameGenerator.FIRST_CHAR.length; int len2 = RandomNameGenerator.NONFIRST_CHAR.length; int count = len1 * (1 + len2); String[] result = generate(ng, count); Set<String> resultSet = Sets.newHashSet(result); // We got as many names as we asked for, and all are different. assertThat(resultSet).hasSize(count); // First come len1 names with length 1, then 2. for (int i = 0; i < len1; ++i) { assertThat(result[i].length()).isEqualTo(1); } // Because there's no prefix, we use the "first characters" alphabet // for the first character, so there will be only len1 length-1 names, // not len2 as in testGenerate. for (int i = len1; i < count; ++i) { // Because we don't have a prefix, some generated names will be // JavaScript keywords and will be skipped, so we'll get a few // length-3 names. assertThat(result[i].length()).isAtLeast(2); } } public static void testPrefix() throws Exception { Random random = new Random(0); Set<String> reservedNames = ImmutableSet.of(); String prefix = "prefix"; RandomNameGenerator ng = new RandomNameGenerator( reservedNames, prefix, null, random); // Generate all 1- and 2-character names. int len1 = RandomNameGenerator.FIRST_CHAR.length; int len2 = RandomNameGenerator.NONFIRST_CHAR.length; int count = len1 * (1 + len2); String[] result = generate(ng, count); for (int i = 0; i < count; ++i) { assertThat(result[i]).startsWith(prefix); assertThat(result[i].length()).isGreaterThan(prefix.length()); } } public static void testSeeds() throws Exception { // Using different seeds should return different names. Random random0 = new Random(0); Random random1 = new Random(1); Set<String> reservedNames = ImmutableSet.of(); RandomNameGenerator ng0 = new RandomNameGenerator( reservedNames, "", null, random0); RandomNameGenerator ng1 = new RandomNameGenerator( reservedNames, "", null, random1); int count = 1000; String[] results0 = generate(ng0, count); String[] results1 = generate(ng1, count); int countPass = 0; int countTest = 0; for (int i = 0; i < count; ++i, ++countTest) { if (!results0[i].equals(results1[i])) { countPass++; } } assertThat(100.0 * countPass / countTest).isGreaterThan(90.0); // arbitrary threshold } public static void testReservedNames() throws Exception { Random random = new Random(0); Set<String> reservedNames = ImmutableSet.of("x", "ba"); RandomNameGenerator ng = new RandomNameGenerator( reservedNames, "", null, random); // Generate all 1- and 2-character names (and a couple 3-character names, // because "x" and "ba", and keywords, shouldn't be used). int count = RandomNameGenerator.FIRST_CHAR.length * (RandomNameGenerator.NONFIRST_CHAR.length + 1); Set<String> result = Sets.newHashSet(generate(ng, count)); assertThat(result).doesNotContain("x"); assertThat(result).doesNotContain("ba"); // Even though we skipped "x" and "ba", we still got 'count' different // names. We know they are different because 'result' is a Set. assertThat(result).hasSize(count); } public static void testReservedCharacters() throws Exception { Random random = new Random(0); Set<String> reservedNames = ImmutableSet.of(); RandomNameGenerator ng = new RandomNameGenerator( reservedNames, "", new char[]{'a', 'b'}, random); // Generate all 1- and 2-character names (and also many 3-character names, // because "a" and "b" shouldn't be used). int count = RandomNameGenerator.FIRST_CHAR.length * (RandomNameGenerator.NONFIRST_CHAR.length + 1); Set<String> result = Sets.newHashSet(generate(ng, count)); assertThat(result).doesNotContain("a"); assertThat(result).doesNotContain("b"); assertThat(result).contains("x"); assertThat(result).doesNotContain("ax"); assertThat(result).doesNotContain("bx"); assertThat(result).doesNotContain("xa"); assertThat(result).doesNotContain("xb"); assertThat(result).contains("xx"); // Even though we skipped a few names with reserved characters, we still // got 'count' different names. We know they are different because 'result' // is a Set. assertThat(result).hasSize(count); } }