/*
* Copyright 2010 Google Inc.
*
* 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.gwt.precompress.linker;
import com.google.gwt.core.ext.LinkerContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.linker.ArtifactSet;
import com.google.gwt.core.ext.linker.ConfigurationProperty;
import com.google.gwt.core.ext.linker.EmittedArtifact;
import com.google.gwt.core.ext.linker.EmittedArtifact.Visibility;
import com.google.gwt.core.ext.linker.SelectionProperty;
import com.google.gwt.core.ext.linker.SyntheticArtifact;
import junit.framework.TestCase;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
/**
* Tests {@link PrecompressLinker}.
*/
public class PrecompressLinkerTest extends TestCase {
private static class MockConfigurationProperty implements
ConfigurationProperty, Comparable<MockConfigurationProperty> {
private boolean hasMultipleValues;
private String name;
private List<String> values = new ArrayList<String>();
public MockConfigurationProperty(String name, boolean hasMultipleValues) {
this.name = name;
this.hasMultipleValues = hasMultipleValues;
}
@Override
public int compareTo(MockConfigurationProperty o) {
return getName().compareTo(o.getName());
}
@Override
public String getName() {
return name;
}
@Override
@Deprecated
public String getValue() {
return values.get(0);
}
@Override
public List<String> getValues() {
return values;
}
@Override
public boolean hasMultipleValues() {
return hasMultipleValues;
}
public void setValue(String value) {
values.clear();
values.add(value);
}
}
private class MockLinkerContext implements LinkerContext {
@Override
public SortedSet<ConfigurationProperty> getConfigurationProperties() {
return new TreeSet<ConfigurationProperty>(Arrays.asList(
propLeaveOriginals, propPathRegexes));
}
@Override
public String getModuleFunctionName() {
return "MockModule";
}
@Override
public long getModuleLastModified() {
return 0;
}
@Override
public String getModuleName() {
return "MockModule";
}
@Override
public SortedSet<SelectionProperty> getProperties() {
return new TreeSet<SelectionProperty>();
}
@Override
public boolean isOutputCompact() {
return true;
}
@Override
public String optimizeJavaScript(TreeLogger logger, String jsProgram) {
return jsProgram;
}
}
private static void assertEqualBytes(byte[] expected, byte[] actual) {
assertEquals(expected.length, actual.length);
for (int i = 0; i < expected.length; i++) {
assertEquals(expected[i], actual[i]);
}
}
private static byte[] compress(byte[] content) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
GZIPOutputStream gzip = new GZIPOutputStream(baos);
InputStream in = new ByteArrayInputStream(content);
byte[] buf = new byte[10000];
int n;
while ((n = in.read(buf)) > 0) {
gzip.write(buf, 0, n);
}
gzip.close();
return baos.toByteArray();
} catch (IOException e) {
throw new RuntimeException(
"Unexpected IO exception from memory operations");
}
}
private static byte[] contents(EmittedArtifact art)
throws UnableToCompleteException, IOException {
InputStream input = art.getContents(TreeLogger.NULL);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buf = new byte[10000];
int n;
while ((n = input.read(buf)) > 0) {
baos.write(buf, 0, n);
}
return baos.toByteArray();
}
private static byte[] decompress(byte[] compressed) throws IOException {
GZIPInputStream gzip = new GZIPInputStream(new ByteArrayInputStream(
compressed));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buf = new byte[10000];
int n;
while ((n = gzip.read(buf)) > 0) {
baos.write(buf, 0, n);
}
return baos.toByteArray();
}
private static SyntheticArtifact emit(String path, byte[] content) {
return new SyntheticArtifact(PrecompressLinker.class, path, content);
}
private static SyntheticArtifact emit(String path, String contents) {
try {
return emit(path, contents.getBytes("UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e.getMessage());
}
}
private static SyntheticArtifact emitPrivate(String string, String contents) {
SyntheticArtifact art = emit(string, contents);
art.setVisibility(Visibility.Private);
return art;
}
private static EmittedArtifact findArtifact(ArtifactSet artifacts, String path) {
for (EmittedArtifact art : artifacts.find(EmittedArtifact.class)) {
if (art.getPartialPath().equals(path)) {
return art;
}
}
return null;
}
/**
* Return a highly compressible string.
*/
private static String fooFileContents() {
StringBuffer buf = new StringBuffer();
for (int i = 0; i < 1000; i++) {
buf.append("another identical line\n");
}
return buf.toString();
}
private static byte[] uncompressibleContent() {
try {
byte[] content = fooFileContents().getBytes("UTF-8");
while (true) {
byte[] updated = compress(content);
if (updated.length >= content.length) {
return content;
}
content = updated;
}
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e.getMessage());
}
}
private ArtifactSet artifacts;
private LinkerContext context = new MockLinkerContext();
private MockConfigurationProperty propLeaveOriginals;
private MockConfigurationProperty propPathRegexes;
/**
* Test that foo.js gets compressed to foo.js.gz, and bar.js is left alone.
*/
public void testBasics() throws UnableToCompleteException, IOException {
ArtifactSet updated = linkArtifacts();
EmittedArtifact foo = findArtifact(updated, "foo.js");
assertNotNull(foo);
EmittedArtifact fooGz = findArtifact(updated, "foo.js.gz");
assertNotNull(fooGz);
assertEqualBytes(contents(foo), decompress(contents(fooGz)));
EmittedArtifact barGz = findArtifact(updated, "bar.js.gz");
assertNull("bar.js is private and should not have been compressed", barGz);
EmittedArtifact uncompressibleGz = findArtifact(updated,
"uncompressible.js.gz");
assertNull(
"uncompressible.js is not compressible and should have been left alone",
uncompressibleGz);
}
/**
* Test that the blacklist takes effect.
*/
public void testBlackList() throws UnableToCompleteException {
propPathRegexes.values.add("-foo\\.js");
ArtifactSet updated = linkArtifacts();
// foo.txt is not in the list of patterns, so don't compress
EmittedArtifact stuffGz = findArtifact(updated, "stuff.txt.gz");
assertNull("stuff.txt should not have been compressed", stuffGz);
// foo.js matches two regexes; the last should win
EmittedArtifact fooGz = findArtifact(updated, "foo.js.gz");
assertNull("foo.js should not have been compressed", fooGz);
}
/**
* Tests that if precompress.leave.original if false, the originals are
* removed.
*/
public void testRemovingOriginals() throws UnableToCompleteException {
propLeaveOriginals.setValue("false");
ArtifactSet updated = linkArtifacts();
EmittedArtifact foo = findArtifact(updated, "foo.js");
assertNull("foo.js should have been removed", foo);
}
@Override
protected void setUp() {
// add some artifacts to test with
artifacts = new ArtifactSet();
artifacts.add(emit("foo.js", fooFileContents()));
artifacts.add(emitPrivate("bar.js", fooFileContents()));
artifacts.add(emit("uncompressible.js", uncompressibleContent()));
artifacts.add(emit("stuff.txt", fooFileContents()));
artifacts.add(emit("data.xml", fooFileContents()));
artifacts.freeze();
propLeaveOriginals = new MockConfigurationProperty(
"precompress.leave.originals", false);
propLeaveOriginals.setValue("true");
propPathRegexes = new MockConfigurationProperty("precompress.path.regexes",
true);
propPathRegexes.values.add(".*\\.html");
propPathRegexes.values.add(".*\\.js");
propPathRegexes.values.add(".*\\.css");
}
private ArtifactSet linkArtifacts() throws UnableToCompleteException {
return new PrecompressLinker().link(TreeLogger.NULL, context, artifacts,
true);
}
}