/* * Copyright (c) 2014, the Dart project authors. * * Licensed under the Eclipse Public License v1.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.eclipse.org/legal/epl-v10.html * * 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.dart.tools.ui.feedback; import com.google.dart.engine.utilities.os.OSUtilities; import com.google.dart.tools.ui.feedback.FeedbackUtils.Stats; import static com.google.dart.tools.ui.feedback.LogReaderTest.EOL; import static com.google.dart.tools.ui.feedback.LogReaderTest.LOG_CONTENTS; import static com.google.dart.tools.ui.feedback.LogReaderTest.LOG_EXCEPTION; import static com.google.dart.tools.ui.feedback.LogReaderTest.LOG_MESSAGE; import static com.google.dart.tools.ui.feedback.LogReaderTest.LOG_SESSION_START; import junit.framework.TestCase; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Display; import userfeedback.Common.CommonData; import userfeedback.Extension.ExtensionSubmit; import userfeedback.Web.ProductSpecificData; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.ServerSocket; import java.net.Socket; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.TimeUnit; public class FeedbackSubmissionJob2Test extends TestCase { private final class MockProgressMonitor implements IProgressMonitor { boolean done; boolean begin; boolean worked; @Override public void beginTask(String name, int totalWork) { begin = true; } @Override public void done() { done = true; } @Override public void internalWorked(double work) { } @Override public boolean isCanceled() { return false; } @Override public void setCanceled(boolean value) { } @Override public void setTaskName(String name) { } @Override public void subTask(String name) { } @Override public void worked(int work) { worked = true; } } // Feedback content private static final String IDE_VERSION = "ideVersion"; private static final String FEEDBACK_TEXT = "feedbackText"; private static final String SPARSE_MAP_KEY = "test-option-key"; private static final String SPARSE_MAP_VALUE = "test-option-value"; private static final String TOKEN_RESPONSE_GOOD = "header stuff to be ignored\n" + " var GF_TOKEN = \"some-token\";\n" // valid token + "trailing stuff to be ignored"; public static FeedbackReport newTestFeedbackReport(String logContents) { return newTestFeedbackReport(FEEDBACK_TEXT, null, logContents); } private static FeedbackReport newTestFeedbackReport(String feedbackText, Image screenshot, String logContents) { Map<String, String> sparseOptionsMap = new HashMap<String, String>(); sparseOptionsMap.put(SPARSE_MAP_KEY, SPARSE_MAP_VALUE); sparseOptionsMap.put("experimental/altKeyBindings", "true"); FeedbackReport report = new FeedbackReport(// feedbackText, "Editor", "osDetails", IDE_VERSION, new Stats(1, 2, 3, 4, 5, 6, "indexStats", true), logContents, screenshot, true, true, sparseOptionsMap); report.setUserEmail("username@gmail.com"); return report; } private ServerSocket serverSocket; private Throwable serverException; private boolean sawReturn = false; boolean errorLogged; private static final String ISO_8859_1 = "ISO-8859-1"; private static final String CRLF = "\r\n"; private static final String FEEDBACK_LOG_1 = LOG_CONTENTS; private static final String FEEDBACK_LOG_2 = LOG_EXCEPTION + EOL + EOL + LOG_MESSAGE + EOL + EOL + LOG_CONTENTS; private static final String FEEDBACK_LOG_3 = LOG_EXCEPTION + EOL + EOL + LOG_MESSAGE; /** * Test by submitting feedback to the real Google Feedback server. This test is not run by default * and is not part of the test suite. All other tests send feedback to a local mock of the Google * Feedback server. */ public static void main(String[] args) { Display display = new Display(); try { Image tinyScreenshot = new Image(display, 50, 50); try { GC gc = new GC(display); gc.copyArea(tinyScreenshot, 0, 0); gc.dispose(); // Show screenshot for debugging // Shell shell = new Shell(display); // shell.setLayout(new RowLayout()); // new Label(shell, SWT.NONE).setText("Feedback image: "); // new Label(shell, SWT.NONE).setImage(tinyScreenshot); // shell.open(); // while (!shell.isDisposed()) { // if (!display.readAndDispatch()) { // display.sleep(); // } // } // shell.dispose(); // Send feedback FeedbackSubmissionJob2 job = new FeedbackSubmissionJob2(newTestFeedbackReport( "Test feedback with a tiny screenshot " + System.currentTimeMillis(), tinyScreenshot, FEEDBACK_LOG_1), true, true, true); job.setTestFeedback(true); try { job.submitFeedback(new NullProgressMonitor()); System.out.println("submitFeedback successful"); } catch (IOException e) { System.out.println("submitFeedback failed"); e.printStackTrace(System.out); } } finally { tinyScreenshot.dispose(); } } finally { display.dispose(); } } private String tokenResponse; private String submitResponse; private final BlockingQueue<Object> requestQueue = new ArrayBlockingQueue<Object>(4); private final Object NO_CONTENT_SENT = new Object(); public void test_getDataAsText() throws Exception { FeedbackSubmissionJob2 job = newTestFeedbackClient(true, FEEDBACK_LOG_1, true, true); String text = job.getDataAsText(); assertTrue(text.contains("OS")); assertTrue(text.contains("altKeyBindings")); assertTrue(text.contains("!SESSION")); } public void test_getDataAsText_noLog() throws Exception { FeedbackSubmissionJob2 job = newTestFeedbackClient(false, FEEDBACK_LOG_1, true, true); String text = job.getDataAsText(); assertTrue(text.contains("OS")); assertTrue(text.contains("altKeyBindings")); assertFalse(text.contains("!SESSION")); } public void test_getFeedbackToken_invalidResponse1() throws Exception { tokenResponse = "header stuff to be ignored\n" // header + " var GF_TOKEN = \"some-token\n" // token missing closing " + "trailing stuff to be ignored"; FeedbackSubmissionJob2 job = newTestFeedbackClient(true, FEEDBACK_LOG_1, true, true); try { assertEquals("some-token", job.getFeedbackToken()); fail("Expected IOException"); } catch (IOException e) { // expected } assertGetTokenProcessed(); } public void test_getFeedbackToken_invalidResponse2() throws Exception { tokenResponse = "header stuff to be ignored\n" // header + " garbled stuff here\n" // garbled token + "trailing stuff to be ignored"; FeedbackSubmissionJob2 job = newTestFeedbackClient(true, FEEDBACK_LOG_1, true, true); try { assertEquals("some-token", job.getFeedbackToken()); fail("Expected IOException"); } catch (IOException e) { // expected } assertGetTokenProcessed(); } public void test_getFeedbackToken_validResponse() throws Exception { //TODO (danrubel): Investigate and fix if (OSUtilities.isLinux()) { return; } tokenResponse = TOKEN_RESPONSE_GOOD; FeedbackSubmissionJob2 job = newTestFeedbackClient(true, FEEDBACK_LOG_1, true, true); assertEquals("some-token", job.getFeedbackToken()); assertGetTokenProcessed(); } public void test_submitFeedback_noLog() throws Exception { //TODO (danrubel): Investigate and fix if (OSUtilities.isLinux()) { return; } tokenResponse = TOKEN_RESPONSE_GOOD; submitResponse = "header stuff \"success\":true trailing stuff"; FeedbackSubmissionJob2 job = newTestFeedbackClient(false, FEEDBACK_LOG_1, true, true); job.submitFeedback(new NullProgressMonitor()); assertGetTokenProcessed(); ExtensionSubmit feedback = waitForFeedbackReceived(); CommonData commonData = feedback.getCommonData(); assertContainsProductSpecificData(commonData, "log", null); assertContainsProductSpecificData(commonData, "logEntryPrevious2", null); assertContainsProductSpecificData(commonData, "logEntryPrevious1", null); assertContainsProductSpecificData(commonData, "logEntryStart", null); assertContainsProductSpecificData(commonData, "logEntry1", null); assertContainsProductSpecificData(commonData, "logEntry2", null); } public void test_submitFeedback_private() throws Exception { //TODO (danrubel): Investigate and fix if (OSUtilities.isLinux()) { return; } tokenResponse = TOKEN_RESPONSE_GOOD; submitResponse = "header stuff \"success\":true trailing stuff"; FeedbackSubmissionJob2 job = newTestFeedbackClient(true, FEEDBACK_LOG_1, true, false); job.submitFeedback(new NullProgressMonitor()); assertGetTokenProcessed(); ExtensionSubmit feedback = waitForFeedbackReceived(); CommonData commonData = feedback.getCommonData(); assertContainsProductSpecificData(commonData, "public", "false"); } public void test_submitFeedback_public() throws Exception { //TODO (danrubel): Investigate and fix if (OSUtilities.isLinux()) { return; } tokenResponse = TOKEN_RESPONSE_GOOD; submitResponse = "header stuff \"success\":true trailing stuff"; FeedbackSubmissionJob2 job = newTestFeedbackClient(true, FEEDBACK_LOG_1, true, true); job.submitFeedback(new NullProgressMonitor()); assertGetTokenProcessed(); ExtensionSubmit feedback = waitForFeedbackReceived(); CommonData commonData = feedback.getCommonData(); assertContainsProductSpecificData(commonData, "public", "true"); } public void test_submitFeedback_success1() throws Exception { //TODO (danrubel): Investigate and fix if (OSUtilities.isLinux()) { return; } tokenResponse = TOKEN_RESPONSE_GOOD; submitResponse = "header stuff \"success\":true trailing stuff"; FeedbackSubmissionJob2 job = newTestFeedbackClient(true, FEEDBACK_LOG_1, true, true); job.submitFeedback(new NullProgressMonitor()); assertGetTokenProcessed(); assertSubmitFeedbackProcessed(); } public void test_submitFeedback_success2() throws Exception { //TODO (danrubel): Investigate and fix if (OSUtilities.isLinux()) { return; } tokenResponse = TOKEN_RESPONSE_GOOD; submitResponse = ""; FeedbackSubmissionJob2 job = newTestFeedbackClient(true, FEEDBACK_LOG_1, true, true); job.submitFeedback(new NullProgressMonitor()); assertGetTokenProcessed(); assertSubmitFeedbackProcessed(); } public void test_submitFeedback_withLog1() throws Exception { //TODO (danrubel): Investigate and fix if (OSUtilities.isLinux()) { return; } tokenResponse = TOKEN_RESPONSE_GOOD; submitResponse = "header stuff \"success\":true trailing stuff"; FeedbackSubmissionJob2 job = newTestFeedbackClient(true, FEEDBACK_LOG_1, true, true); job.submitFeedback(new NullProgressMonitor()); assertGetTokenProcessed(); ExtensionSubmit feedback = waitForFeedbackReceived(); CommonData commonData = feedback.getCommonData(); assertContainsProductSpecificData(commonData, "log", FEEDBACK_LOG_1); assertContainsProductSpecificData(commonData, "logEntryPrevious2", null); assertContainsProductSpecificData(commonData, "logEntryPrevious1", null); assertContainsProductSpecificData(commonData, "logEntryStart", LOG_SESSION_START); assertContainsProductSpecificData(commonData, "logEntry1", LOG_MESSAGE); assertContainsProductSpecificData(commonData, "logEntry2", LOG_EXCEPTION); } public void test_submitFeedback_withLog2() throws Exception { //TODO (danrubel): Investigate and fix if (OSUtilities.isLinux()) { return; } tokenResponse = TOKEN_RESPONSE_GOOD; submitResponse = "header stuff \"success\":true trailing stuff"; FeedbackSubmissionJob2 job = newTestFeedbackClient(true, FEEDBACK_LOG_2, true, true); job.submitFeedback(new NullProgressMonitor()); assertGetTokenProcessed(); ExtensionSubmit feedback = waitForFeedbackReceived(); CommonData commonData = feedback.getCommonData(); assertContainsProductSpecificData(commonData, "log", FEEDBACK_LOG_2); assertContainsProductSpecificData(commonData, "logEntryPrevious2", LOG_EXCEPTION); assertContainsProductSpecificData(commonData, "logEntryPrevious1", LOG_MESSAGE); assertContainsProductSpecificData(commonData, "logEntryStart", LOG_SESSION_START); assertContainsProductSpecificData(commonData, "logEntry1", LOG_MESSAGE); assertContainsProductSpecificData(commonData, "logEntry2", LOG_EXCEPTION); } public void test_submitFeedback_withLog3() throws Exception { //TODO (danrubel): Investigate and fix if (OSUtilities.isLinux()) { return; } tokenResponse = TOKEN_RESPONSE_GOOD; submitResponse = "header stuff \"success\":true trailing stuff"; FeedbackSubmissionJob2 job = newTestFeedbackClient(true, FEEDBACK_LOG_3, true, true); job.submitFeedback(new NullProgressMonitor()); assertGetTokenProcessed(); ExtensionSubmit feedback = waitForFeedbackReceived(); CommonData commonData = feedback.getCommonData(); assertContainsProductSpecificData(commonData, "log", FEEDBACK_LOG_3); assertContainsProductSpecificData(commonData, "logEntryPrevious2", null); assertContainsProductSpecificData(commonData, "logEntryPrevious1", null); assertContainsProductSpecificData(commonData, "logEntryStart", null); assertContainsProductSpecificData(commonData, "logEntry1", LOG_EXCEPTION); assertContainsProductSpecificData(commonData, "logEntry2", LOG_MESSAGE); } public void test_submitFeedback_withProgress1() throws Exception { //TODO (danrubel): Investigate and fix if (OSUtilities.isLinux()) { return; } tokenResponse = TOKEN_RESPONSE_GOOD; submitResponse = ""; FeedbackSubmissionJob2 job = newTestFeedbackClient(true, FEEDBACK_LOG_1, true, true); MockProgressMonitor monitor = new MockProgressMonitor(); IStatus result = job.run(monitor); assertGetTokenProcessed(); assertSubmitFeedbackProcessed(); assertTrue(monitor.begin); assertTrue(monitor.worked); assertTrue(monitor.done); assertTrue(result.isOK()); } public void test_submitFeedback_withProgress2() throws Exception { tokenResponse = "random stuff"; FeedbackSubmissionJob2 job = newTestFeedbackClient(true, FEEDBACK_LOG_1, true, true); MockProgressMonitor monitor = new MockProgressMonitor(); IStatus result = job.run(monitor); assertFalse(result.isOK()); } @Override protected void setUp() throws Exception { super.setUp(); startServer(); } @Override protected void tearDown() throws Exception { stopServer(); if (serverException != null) { serverException.printStackTrace(); fail(getClass().getSimpleName() + " Server Exception"); } super.tearDown(); } private void assertContainsProductSpecificData(CommonData commonData, String key, String expectedValue) { List<String> foundValues = getProductSpecificData(commonData, key); for (String value : foundValues) { if (value.equals(expectedValue)) { return; } } if (foundValues.isEmpty()) { if (expectedValue == null) { return; } fail("Failed to find key: " + key); } else { fail("Expected value: " + expectedValue + " but found: " + foundValues); } } private void assertGetTokenProcessed() throws InterruptedException { String request = (String) requestQueue.poll(100, TimeUnit.MILLISECONDS); assertNotNull(request); Object content = requestQueue.poll(100, TimeUnit.MILLISECONDS); assertSame(NO_CONTENT_SENT, content); } private void assertSubmitFeedbackProcessed() throws InterruptedException { ExtensionSubmit feedback = waitForFeedbackReceived(); assertEquals(FeedbackSubmissionJob2.DART_EDITOR_PRODUCT_ID, feedback.getProductId()); CommonData commonData = feedback.getCommonData(); assertEquals(IDE_VERSION, commonData.getProductVersion()); assertEquals(FEEDBACK_TEXT, commonData.getDescription()); assertContainsProductSpecificData(commonData, "option/" + SPARSE_MAP_KEY, SPARSE_MAP_VALUE); } private List<String> getProductSpecificData(CommonData commonData, String key) { List<String> foundValues = new ArrayList<String>(); for (ProductSpecificData data : commonData.getProductSpecificDataList()) { if (data.getKey().equals(key)) { String value = data.getValue(); if (value != null) { foundValues.add(value); } } } return foundValues; } private FeedbackSubmissionJob2 newTestFeedbackClient(boolean includeLog, String logContents, boolean includeScreenshot, boolean isPublic) { return new FeedbackSubmissionJob2( newTestFeedbackReport(logContents), includeLog, includeScreenshot, isPublic, "http://127.0.0.1:" + serverSocket.getLocalPort() + "/token", "http://127.0.0.1:" + serverSocket.getLocalPort() + "/feedback?") { @Override protected void logError(IOException e) { // ignored } }; } private void processRequest(Socket socket) throws IOException { StringBuilder request = new StringBuilder(); Object parseResult = NO_CONTENT_SENT; int responseCode; String responseText; // Get the request InputStream in = socket.getInputStream(); try { String line = readLine(in); if (line == null) { return; } request.append(line); request.append(CRLF); if (line.startsWith("GET /token ") && tokenResponse != null) { responseCode = HttpURLConnection.HTTP_OK; responseText = tokenResponse; } else if (line.startsWith("POST /feedback?some-token") && submitResponse != null) { if (submitResponse.length() == 0) { responseCode = 204; // HTTP_STATUS_OK_NO_CONTENT } else { responseCode = HttpURLConnection.HTTP_OK; } responseText = submitResponse; } else { responseCode = 404; responseText = "Unrecognized Request: " + line; } // Consume remaining input while (true) { try { line = readLine(in); } catch (IOException e) { // ignore additional input break; } if (line == null || line.isEmpty()) { break; } request.append(line); request.append(CRLF); // Read following content as bytes not string if (line.startsWith("Content-Length:")) { int byteCount = Integer.parseInt(line.substring(line.indexOf(':') + 1).trim()); byte[] content = new byte[byteCount]; in.skip(3); // LF CR LF if (in.read(content) != byteCount) { throw new RuntimeException("Failed to read content"); } // Echo bytes for debugging // for (int count = 0; count < byteCount; count++) { // int value = content[count]; // if (value < 0) { // value += 0x100; // } // String text = Integer.toHexString(value).toUpperCase(); // if (text.length() < 2) { // request.append("0"); // } // request.append(text); // request.append(" "); // if (count % 16 == 15) { // request.append(CRLF); // } // } // if (byteCount % 16 != 0) { // request.append(CRLF); // } try { parseResult = ExtensionSubmit.parseFrom(content); } catch (IOException e) { serverException = e; } break; } } // Echo the request for debugging // System.out.println("=================================="); // System.out.println(request); // Build a response StringBuilder builder = new StringBuilder(); builder.append("HTTP/1.1 " + responseCode + " " + "OK" + CRLF); // HTTP/1.1 200 OK builder.append(CRLF); builder.append(responseText); builder.append(CRLF); // Send response OutputStream out = socket.getOutputStream(); try { out.write(builder.toString().getBytes(ISO_8859_1)); out.flush(); } finally { out.close(); } } finally { in.close(); } requestQueue.add(request.toString()); requestQueue.add(parseResult); } private String readLine(InputStream in) throws IOException { StringBuilder result = new StringBuilder(80); while (true) { int ch = in.read(); if (ch == -1) { // EOS break; } else if (ch == '\r') { // CR sawReturn = true; break; } else if (ch == '\n') { // LF if (sawReturn) { sawReturn = false; } else { break; } } else { sawReturn = false; result.append((char) ch); } } return result.toString(); } private void startServer() throws IOException { final String serverName = getClass().getSimpleName() + " Server"; serverSocket = new ServerSocket(0); new Thread(new Runnable() { @Override public void run() { while (true) { Socket socket; try { socket = serverSocket.accept(); } catch (IOException e) { // socket closed as part of teardown break; } try { processRequest(socket); } catch (Throwable e) { serverException = e; } } } }, serverName).start(); } private void stopServer() { try { serverSocket.close(); } catch (IOException exception) { // ignored } } private ExtensionSubmit waitForFeedbackReceived() throws InterruptedException { String request = (String) requestQueue.poll(100, TimeUnit.MILLISECONDS); assertNotNull(request); assertTrue(request.indexOf("Content-Type: application/x-protobuf") > 0); Object content = requestQueue.poll(100, TimeUnit.MILLISECONDS); assertNotSame(NO_CONTENT_SENT, content); return (ExtensionSubmit) content; } }