diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000000..d29f0b121805 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "okhttp-hpacktests/src/test/resources/hpack-test-case"] + path = okhttp-hpacktests/src/test/resources/hpack-test-case + url = git://github.com/http2jp/hpack-test-case.git diff --git a/okhttp-hpacktests/README.md b/okhttp-hpacktests/README.md new file mode 100644 index 000000000000..f6b904996144 --- /dev/null +++ b/okhttp-hpacktests/README.md @@ -0,0 +1,22 @@ +OkHttp HPACK tests +================== + +These tests use the [hpack-test-case][1] project to validate OkHttp's HPACK +implementation. The HPACK test cases are in a separate git submodule, so to +initialize them, you must run: + + git submodule init + git submodule update + +When new interop tests are available, you should update +HpackDecodeInteropGoodTest#GOOD_INTEROP_TESTS with the directory name. + +TODO +---- + + * Add maven goal to avoid manual call to git submodule init. + * Make hpack-test-case update itself from git, and run new tests. + * Add maven goal to generate stories and a pull request to hpack-test-case + to have others validate our output. + +[1]: https://github.com/http2jp/hpack-test-case diff --git a/okhttp-hpacktests/pom.xml b/okhttp-hpacktests/pom.xml new file mode 100644 index 000000000000..0e8849076c26 --- /dev/null +++ b/okhttp-hpacktests/pom.xml @@ -0,0 +1,57 @@ + + + + 4.0.0 + + + com.squareup.okhttp + parent + 2.0.1-SNAPSHOT + + + okhttp-hpacktests + OkHttp HPACK Tests + + + + com.squareup.okio + okio + + + com.squareup.okhttp + okhttp + ${project.version} + + + junit + junit + test + + + com.squareup.okhttp + mockwebserver + ${project.version} + test + + + + com.google.code.gson + gson + 2.2.4 + compile + + + + + + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + + + + diff --git a/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/HpackDecodeInteropBadTest.java b/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/HpackDecodeInteropBadTest.java new file mode 100644 index 000000000000..8d611ac91b09 --- /dev/null +++ b/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/HpackDecodeInteropBadTest.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2014 Square, 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.squareup.okhttp.internal.spdy; + +import com.squareup.okhttp.internal.spdy.hpackjson.Story; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Collection; + +/** + * Known bad tests for HPACK interop. + */ +// TODO: fix these tests (see if the input/test is legit, fix the implementation.) +@Ignore +@RunWith(Parameterized.class) +public class HpackDecodeInteropBadTest extends HpackDecodeTestBase { + + private static final String[] BAD_INTEROP_TESTS = { "go-hpack", "haskell-http2-diff-huffman", + "haskell-http2-linear-huffman", "haskell-http2-naive-huffman", + "haskell-http2-static-huffman", "node-http2-protocol", "twitter-hpack" }; + + public HpackDecodeInteropBadTest(Story story) { + super(story); + } + + @Parameterized.Parameters(name="{0}") + public static Collection createStories() throws Exception { + return createStories(BAD_INTEROP_TESTS); + } + + @Test + public void testGoodDecoderInterop() throws Exception { + testDecoder(); + } +} diff --git a/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/HpackDecodeInteropGoodTest.java b/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/HpackDecodeInteropGoodTest.java new file mode 100644 index 000000000000..bf29e9e5c006 --- /dev/null +++ b/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/HpackDecodeInteropGoodTest.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2014 Square, 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.squareup.okhttp.internal.spdy; + +import com.squareup.okhttp.internal.spdy.hpackjson.Story; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Collection; + +/** + * Known good tests for HPACK interop. + */ +@RunWith(Parameterized.class) +public class HpackDecodeInteropGoodTest extends HpackDecodeTestBase { + + + private static final String[] GOOD_INTEROP_TESTS = { "haskell-http2-diff", + "haskell-http2-linear", "haskell-http2-naive", "haskell-http2-static", + "hyper-hpack", "nghttp2", "nghttp2-16384-4096", + "nghttp2-change-table-size", "node-http2-hpack" }; + + public HpackDecodeInteropGoodTest(Story story) { + super(story); + } + + @Parameterized.Parameters(name="{0}") + public static Collection createStories() throws Exception { + return createStories(GOOD_INTEROP_TESTS); + } + + @Test + public void testGoodDecoderInterop() throws Exception { + testDecoder(); + } +} diff --git a/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/HpackDecodeTestBase.java b/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/HpackDecodeTestBase.java new file mode 100644 index 000000000000..f1d68a3f3875 --- /dev/null +++ b/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/HpackDecodeTestBase.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2014 Square, 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.squareup.okhttp.internal.spdy; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import com.squareup.okhttp.internal.spdy.hpackjson.Case; +import com.squareup.okhttp.internal.spdy.hpackjson.HpackJsonUtil; +import com.squareup.okhttp.internal.spdy.hpackjson.Story; +import okio.Buffer; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; + +/** + * Tests Hpack implementation using https://github.com/http2jp/hpack-test-case/ + */ +public class HpackDecodeTestBase { + + /** + * Reads all stories in the folders provided, asserts if no story found. + */ + protected static Collection createStories(String[] interopTests) + throws Exception { + List result = new ArrayList<>(); + for (String interopTestName : interopTests) { + List stories = HpackJsonUtil.readStories(interopTestName); + if (stories.isEmpty()) { + fail("No stories for: " + interopTestName); + } + for (Story story : stories) { + result.add(new Story[] { story }); + } + } + return result; + } + + private final Buffer bytesIn = new Buffer(); + private final HpackDraft08.Reader hpackReader = new HpackDraft08.Reader(4096, bytesIn); + + private final Story story; + + public HpackDecodeTestBase(Story story) { + this.story = story; + } + + /** + * Expects wire to be set for all cases, and compares the decoder's output to + * expected headers. + */ + protected void testDecoder() throws Exception { + testDecoder(story); + } + + protected void testDecoder(Story story) throws Exception { + for (Case caze : story.getCases()) { + bytesIn.write(caze.getWire()); + hpackReader.readHeaders(); + hpackReader.emitReferenceSet(); + assertSetEquals(String.format("seqno=%d", caze.getSeqno()), caze.getHeaders(), + hpackReader.getAndReset()); + } + } + /** + * Checks if {@code expected} and {@code observed} are equal when viewed as a + * set and headers are deduped. + * + * TODO: See if duped headers should be preserved on decode and verify. + */ + private static void assertSetEquals( + String message, List
expected, List
observed) { + assertEquals(message, new LinkedHashSet<>(expected), new LinkedHashSet<>(observed)); + } + + protected Story getStory() { + return story; + } +} diff --git a/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/HpackRoundTripTest.java b/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/HpackRoundTripTest.java new file mode 100644 index 000000000000..c55bb8b27895 --- /dev/null +++ b/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/HpackRoundTripTest.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2014 Square, 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.squareup.okhttp.internal.spdy; + +import com.squareup.okhttp.internal.spdy.hpackjson.Case; +import com.squareup.okhttp.internal.spdy.hpackjson.Story; +import okio.Buffer; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Collection; + +/** + * Tests for round-tripping headers through hpack.. + */ +// TODO: update hpack-test-case with the output of our encoder. +// This test will hide complementary bugs in the encoder and decoder, +// We should test that the encoder is producing responses that are +// d] +@RunWith(Parameterized.class) +public class HpackRoundTripTest extends HpackDecodeTestBase { + + private static final String[] RAW_DATA = { "raw-data" }; + + @Parameterized.Parameters(name="{0}") + public static Collection getStories() throws Exception { + return createStories(RAW_DATA); + } + + private Buffer bytesOut = new Buffer(); + private HpackDraft08.Writer hpackWriter = new HpackDraft08.Writer(bytesOut); + + public HpackRoundTripTest(Story story) { + super(story); + } + + @Test + public void testRoundTrip() throws Exception { + Story story = getStory().clone(); + // Mutate cases in base class. + for (Case caze : story.getCases()) { + hpackWriter.writeHeaders(caze.getHeaders()); + caze.setWire(bytesOut.readByteString()); + } + + testDecoder(story); + } + +} diff --git a/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/hpackjson/Case.java b/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/hpackjson/Case.java new file mode 100644 index 000000000000..d5d272872cca --- /dev/null +++ b/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/hpackjson/Case.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2014 Square, 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.squareup.okhttp.internal.spdy.hpackjson; + +import com.squareup.okhttp.internal.spdy.Header; +import okio.ByteString; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Representation of an individual case (set of headers and wire format). + * There are many cases for a single story. This class is used reflectively + * with Gson to parse stories. + */ +public class Case implements Cloneable { + + private int seqno; + private String wire; + private List> headers; + + public List
getHeaders() { + List
result = new ArrayList<>(); + for (Map inputHeader : headers) { + Map.Entry entry = inputHeader.entrySet().iterator().next(); + result.add(new Header(entry.getKey(), entry.getValue())); + } + return result; + } + + public ByteString getWire() { + return ByteString.decodeHex(wire); + } + + public int getSeqno() { + return seqno; + } + + public void setWire(ByteString wire) { + this.wire = wire.hex(); + } + + @Override + protected Case clone() throws CloneNotSupportedException { + Case result = new Case(); + result.seqno = seqno; + result.wire = wire; + result.headers = new ArrayList<>(); + for (Map header : headers) { + result.headers.add(new LinkedHashMap(header)); + } + return result; + } +} diff --git a/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/hpackjson/HpackJsonUtil.java b/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/hpackjson/HpackJsonUtil.java new file mode 100644 index 000000000000..d0695446d5c8 --- /dev/null +++ b/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/hpackjson/HpackJsonUtil.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2014 Square, 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.squareup.okhttp.internal.spdy.hpackjson; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; + +/** + * Utilities for reading HPACK tests. + */ +public final class HpackJsonUtil { + + private static final String STORY_RESOURCE_FORMAT = + "/hpack-test-case/%s/story_%02d.json"; + + private static final Gson GSON = new GsonBuilder().create(); + + private static Story readStory(InputStream jsonResource) throws Exception { + return GSON.fromJson(new InputStreamReader(jsonResource, "UTF-8"), Story.class); + } + + /** + * Reads stories named "story_xx.json" from the folder provided. + */ + public static List readStories(String testFolderName) throws Exception { + List result = new ArrayList<>(); + int i = 0; + while (true) { // break after last test. + String storyResourceName = String.format(STORY_RESOURCE_FORMAT, testFolderName, i); + InputStream storyInputStream = HpackJsonUtil.class.getResourceAsStream(storyResourceName); + if (storyInputStream == null) { + break; + } + try { + Story story = readStory(storyInputStream); + story.setFileName(storyResourceName); + result.add(story); + i++; + } finally { + storyInputStream.close(); + } + } + return result; + } + + private HpackJsonUtil() { } // Utilities only. +} \ No newline at end of file diff --git a/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/hpackjson/Story.java b/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/hpackjson/Story.java new file mode 100644 index 000000000000..e7898cb82659 --- /dev/null +++ b/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/hpackjson/Story.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2014 Square, 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.squareup.okhttp.internal.spdy.hpackjson; + +import java.util.ArrayList; +import java.util.List; + +/** + * Representation of one story, a set of request headers to encode or decode. + * This class is used reflectively with Gson to parse stories from files. + */ +public class Story implements Cloneable { + + private transient String fileName; + private List cases; + private int draft; + private String description; + + /** + * The filename is only used in the toString representation. + */ + void setFileName(String fileName) { + this.fileName = fileName; + } + + public List getCases() { + return cases; + } + + @Override + public Story clone() throws CloneNotSupportedException { + Story story = new Story(); + story.fileName = this.fileName; + story.cases = new ArrayList<>(); + for (Case caze : cases) { + story.cases.add(caze.clone()); + } + story.draft = draft; + story.description = description; + return story; + } + + @Override + public String toString() { + // Used as the test name. + return fileName; + } +} diff --git a/okhttp-hpacktests/src/test/resources/hpack-test-case b/okhttp-hpacktests/src/test/resources/hpack-test-case new file mode 160000 index 000000000000..bc5da28a1163 --- /dev/null +++ b/okhttp-hpacktests/src/test/resources/hpack-test-case @@ -0,0 +1 @@ +Subproject commit bc5da28a11638c460c39b763f44fe230472c1b24 diff --git a/pom.xml b/pom.xml index 8ef3e975c160..5a1563b43f05 100644 --- a/pom.xml +++ b/pom.xml @@ -21,6 +21,7 @@ okhttp okhttp-apache + okhttp-hpacktests okhttp-tests okhttp-urlconnection okcurl