/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */


#include "pagespeed/kernel/js/js_minify.h"


#include "pagespeed/kernel/base/google_message_handler.h"
#include "pagespeed/kernel/base/gtest.h"
#include "pagespeed/kernel/base/stdio_file_system.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/base/string_util.h"
#include "pagespeed/kernel/js/js_keywords.h"

namespace {

using net_instaweb::StrAppend;

// This sample code comes from Douglas Crockford's jsmin example.
const char* kBeforeCompilation =
    "// is.js\n"
    "\n"
    "// (c) 2001 Douglas Crockford\n"
    "// 2001 June 3\n"
    "\n"
    "\n"
    "// is\n"
    "\n"
    "// The -is- object is used to identify the browser.  "
    "Every browser edition\n"
    "// identifies itself, but there is no standard way of doing it, "
    "and some of\n"
    "// the identification is deceptive. This is because the authors of web\n"
    "// browsers are liars. For example, Microsoft's IE browsers claim to be\n"
    "// Mozilla 4. Netscape 6 claims to be version 5.\n"
    "\n"
    "var is = {\n"
    "    ie:      navigator.appName == 'Microsoft Internet Explorer',\n"
    "    java:    navigator.javaEnabled(),\n"
    "    ns:      navigator.appName == 'Netscape',\n"
    "    ua:      navigator.userAgent.toLowerCase(),\n"
    "    version: parseFloat(navigator.appVersion.substr(21)) ||\n"
    "             parseFloat(navigator.appVersion),\n"
    "    win:     navigator.platform == 'Win32'\n"
    "}\n"
    "is.mac = is.ua.indexOf('mac') >= 0;\n"
    "if (is.ua.indexOf('opera') >= 0) {\n"
    "    is.ie = is.ns = false;\n"
    "    is.opera = true;\n"
    "}\n"
    "if (is.ua.indexOf('gecko') >= 0) {\n"
    "    is.ie = is.ns = false;\n"
    "    is.gecko = true;\n"
    "}\n";

const char* kAfterCompilationOld =
    "var is={ie:navigator.appName=='Microsoft Internet Explorer',"
    "java:navigator.javaEnabled(),ns:navigator.appName=='Netscape',"
    "ua:navigator.userAgent.toLowerCase(),version:parseFloat("
    "navigator.appVersion.substr(21))||parseFloat(navigator.appVersion)"
    ",win:navigator.platform=='Win32'}\n"
    "is.mac=is.ua.indexOf('mac')>=0;if(is.ua.indexOf('opera')>=0){"
    "is.ie=is.ns=false;is.opera=true;}\n"
    "if(is.ua.indexOf('gecko')>=0){is.ie=is.ns=false;is.gecko=true;}";

const char* kAfterCompilationNew =
    "var is={ie:navigator.appName=='Microsoft Internet Explorer',"
    "java:navigator.javaEnabled(),ns:navigator.appName=='Netscape',"
    "ua:navigator.userAgent.toLowerCase(),version:parseFloat("
    "navigator.appVersion.substr(21))||parseFloat(navigator.appVersion)"
    ",win:navigator.platform=='Win32'}\n"
    "is.mac=is.ua.indexOf('mac')>=0;if(is.ua.indexOf('opera')>=0){"
    "is.ie=is.ns=false;is.opera=true;}"
    "if(is.ua.indexOf('gecko')>=0){is.ie=is.ns=false;is.gecko=true;}";

const char kTestRootDir[] = "/pagespeed/kernel/js/testdata/third_party/";

class JsMinifyTest : public testing::Test {
 protected:
  void CheckOldMinification(StringPiece before, StringPiece after) {
    GoogleString output;
    EXPECT_TRUE(pagespeed::js::MinifyJs(before, &output));
    EXPECT_EQ(after, output);

    int output_size = -1;
    EXPECT_TRUE(pagespeed::js::GetMinifiedJsSize(before, &output_size));
    EXPECT_EQ(static_cast<int>(after.size()), output_size);
  }

  void CheckNewMinification(StringPiece before, StringPiece after) {
    GoogleString output;
    EXPECT_TRUE(pagespeed::js::MinifyUtf8Js(&patterns_, before, &output));
    EXPECT_EQ(after, output);
  }

  void CheckMinification(StringPiece before, StringPiece after) {
    CheckOldMinification(before, after);
    CheckNewMinification(before, after);
  }

  void CheckOldError(StringPiece input) {
    GoogleString output;
    EXPECT_FALSE(pagespeed::js::MinifyJs(input, &output));

    int output_size = -1;
    EXPECT_FALSE(pagespeed::js::GetMinifiedJsSize(input, &output_size));
    EXPECT_EQ(-1, output_size);
  }

  void CheckNewError(StringPiece input) {
    GoogleString output;
    EXPECT_FALSE(pagespeed::js::MinifyUtf8Js(&patterns_, input, &output));
  }

  void CheckError(StringPiece input) {
    CheckOldError(input);
    CheckNewError(input);
  }

  void CheckFileMinification(StringPiece before_filename,
                             StringPiece after_filename) {
    net_instaweb::StdioFileSystem file_system;
    net_instaweb::GoogleMessageHandler message_handler;
    GoogleString original;
    {
      const GoogleString filepath = net_instaweb::StrCat(
          net_instaweb::GTestSrcDir(), kTestRootDir, before_filename);
      ASSERT_TRUE(file_system.ReadFile(
          filepath.c_str(), &original, &message_handler));
    }
    GoogleString expected;
    {
      const GoogleString filepath = net_instaweb::StrCat(
          net_instaweb::GTestSrcDir(), kTestRootDir, after_filename);
      ASSERT_TRUE(file_system.ReadFile(
          filepath.c_str(), &expected, &message_handler));
    }
    GoogleString actual;
    EXPECT_TRUE(pagespeed::js::MinifyUtf8Js(&patterns_, original, &actual));
    EXPECT_STREQ(expected, actual);
  }

  pagespeed::js::JsTokenizerPatterns patterns_;
};

TEST_F(JsMinifyTest, Basic) {
  // Our new minifier is slightly better at removing linebreaks than our old
  // minifier, so they get slightly different results for this test.
  CheckOldMinification(kBeforeCompilation, kAfterCompilationOld);
  CheckNewMinification(kBeforeCompilation, kAfterCompilationNew);
}

TEST_F(JsMinifyTest, AlreadyMinified) {
  CheckMinification(kAfterCompilationNew, kAfterCompilationNew);
}

TEST_F(JsMinifyTest, Json) {
  CheckMinification("{  \"foo\": {\"bar\": 1}, \"baz\": 2 } ",
                    "{\"foo\":{\"bar\":1},\"baz\":2}");
}

TEST_F(JsMinifyTest, ErrorUnclosedComment) {
  CheckError("/* not valid javascript");
}

TEST_F(JsMinifyTest, ErrorUnclosedString) {
  CheckError("\"not valid javascript");
}

TEST_F(JsMinifyTest, ErrorUnclosedRegex) {
  CheckError("/not_valid_javascript");
}

TEST_F(JsMinifyTest, ErrorRegexNewline) {
  CheckError("/not_valid\njavascript/;");
}

TEST_F(JsMinifyTest, SignedCharDoesntSignExtend) {
  const unsigned char input[] = { 0xe0, 0xb2, 0xa0, 0x00 };
  const char* input_nosign = reinterpret_cast<const char*>(input);
  CheckMinification(input_nosign, input_nosign);
}

TEST_F(JsMinifyTest, DealWithCrlf) {
  CheckMinification("var x = 1;\r\nvar y = 2;", "var x=1;var y=2;");
}

TEST_F(JsMinifyTest, DealWithTabs) {
  CheckMinification("var x = 1;\n\tvar y = 2;", "var x=1;var y=2;");
}

TEST_F(JsMinifyTest, EscapedCrlfInStringLiteral) {
  CheckMinification("var x = 'foo\\\r\nbar';", "var x='foo\\\r\nbar';");
}

TEST_F(JsMinifyTest, EmptyInput) {
  CheckMinification("", "");
}

TEST_F(JsMinifyTest, TreatCarriageReturnAsLinebreak) {
  CheckMinification("x = 1\ry = 2", "x=1\ny=2");
}

// See http://code.google.com/p/page-speed/issues/detail?id=607
TEST_F(JsMinifyTest, CarriageReturnEndsLineComment) {
  CheckMinification("x = 1 // foobar\ry = 2", "x=1\ny=2");
}

// See http://code.google.com/p/page-speed/issues/detail?id=198
TEST_F(JsMinifyTest, LeaveIEConditionalCompilationComments) {
  // Our new minifier is slightly better at removing linebreaks than our old
  // minifier, so they get slightly different results for this test.
  CheckOldMinification(
      "/*@cc_on\n"
      "  /*@if (@_win32)\n"
      "    document.write('IE');\n"
      "  @else @*/\n"
      "    document.write('other');\n"
      "  /*@end\n"
      "@*/",
      "/*@cc_on\n"
      "  /*@if (@_win32)\n"
      "    document.write('IE');\n"
      "  @else @*/\n"
      "document.write('other');/*@end\n"
      "@*/");
  CheckNewMinification(
      "/*@cc_on\n"
      "  /*@if (@_win32)\n"
      "    document.write('IE');\n"
      "  @else @*/\n"
      "    document.write('other');\n"
      "  /*@end\n"
      "@*/",
      "/*@cc_on\n"
      "  /*@if (@_win32)\n"
      "    document.write('IE');\n"
      "  @else @*/document.write('other');/*@end\n"
      "@*/");
}

TEST_F(JsMinifyTest, DoNotJoinPlusses) {
  CheckMinification("var x = 'date=' + +new Date();",
                    "var x='date='+ +new Date();");
}

TEST_F(JsMinifyTest, DoNotJoinPlusAndPlusPlus) {
  CheckMinification("var x = y + ++z;", "var x=y+ ++z;");
}

TEST_F(JsMinifyTest, DoNotJoinPlusPlusAndPlus) {
  CheckMinification("var x = y++ + z;", "var x=y++ +z;");
}

TEST_F(JsMinifyTest, DoNotJoinMinuses) {
  CheckMinification("var x = 'date=' - -new Date();",
                    "var x='date='- -new Date();");
}

TEST_F(JsMinifyTest, DoNotJoinMinusAndMinusMinus) {
  CheckMinification("var x = y - --z;", "var x=y- --z;");
}

TEST_F(JsMinifyTest, DoNotJoinMinusMinusAndMinus) {
  CheckMinification("var x = y-- - z;", "var x=y-- -z;");
}

TEST_F(JsMinifyTest, DoJoinBangs) {
  CheckMinification("var x = ! ! y;", "var x=!!y;");
}

// See http://code.google.com/p/page-speed/issues/detail?id=242
TEST_F(JsMinifyTest, RemoveSurroundingSgmlComment) {
  CheckMinification("<!--\nvar x = 42;\n//-->", "var x=42;");
}

TEST_F(JsMinifyTest, RemoveSurroundingSgmlCommentWithoutSlashSlash) {
  CheckMinification("<!--\nvar x = 42;\n-->\n", "var x=42;");
}

// See http://code.google.com/p/page-speed/issues/detail?id=242
TEST_F(JsMinifyTest, SgmlLineComment) {
  CheckMinification("var x = 42; <!-- comment\nvar y = 17;",
                    "var x=42;var y=17;");
}

TEST_F(JsMinifyTest, RemoveSgmlCommentCloseOnOwnLine1) {
  CheckMinification("var x = 42;\n    --> \n", "var x=42;");
}

TEST_F(JsMinifyTest, RemoveSgmlCommentCloseOnOwnLine2) {
  CheckMinification("-->\nvar x = 42;\n", "var x=42;");
}

TEST_F(JsMinifyTest, DoNotRemoveSgmlCommentCloseInMidLine) {
  CheckMinification("var x = 42; --> \n", "var x=42;-->");
}

TEST_F(JsMinifyTest, DoNotCreateLineComment) {
  // Yes, this is legal code.  It sets x to NaN.
  CheckMinification("var x = 42 / /foo/;\n", "var x=42/ /foo/;");
}

TEST_F(JsMinifyTest, DoNotCreateSgmlLineComment1) {
  // Yes, this is legal code.  It tests if x is less than not(decrement y).
  CheckMinification("if (x <! --y) { x = 0; }\n", "if(x<! --y){x=0;}");
}

TEST_F(JsMinifyTest, DoNotCreateSgmlLineComment2) {
  // Yes, this is legal code.  It tests if x is less than not(decrement y).
  CheckMinification("if (x < !--y) { x = 0; }\n", "if(x< !--y){x=0;}");
}

TEST_F(JsMinifyTest, DoNotJoinDecimalIntegerAndDot) {
  // 34 .toString() is legal code, but 34.toString() isn't, because the . in
  // the second example gets parsed as part of the literal (decimal point).  So
  // we need to leave a space in there.  Our old minifier gets this wrong, but
  // the new minifier should handle it correctly.
  CheckNewMinification("0192  . toString()", "0192 .toString()");
}

TEST_F(JsMinifyTest, DoJoinHexOctalIntegerAndDot) {
  // On the other hand, hex and octal literals can't have decimal points, so we
  // don't need the space here.
  CheckMinification("0x3e2  . toString() + 0172  . toString()",
                    "0x3e2.toString()+0172.toString()");
}

TEST_F(JsMinifyTest, DoJoinDecimalFractionAndDot) {
  // Also, if the decimal literal can't take another decimal point, then we can
  // safely remove the space.
  CheckMinification("3.5 . toString() + 3e2 . toString()",
                    "3.5.toString()+3e2.toString()");
}

TEST_F(JsMinifyTest, TrickyRegexLiteral) {
  // The first assignment is two divisions; the second assignment is a regex
  // literal.  JSMin gets this wrong (it removes whitespace from the regex).
  CheckMinification("var x = a[0] / b /i;\n var y = a[0] + / b /i;",
                    "var x=a[0]/b/i;var y=a[0]+/ b /i;");
}

TEST_F(JsMinifyTest, ObjectLiteralRegexLiteral) {
  // On the first line, this looks like it should be an object literal divided
  // by x divided by i, but nope, that's a block with a labelled expression
  // statement, followed by a regex literal.  The second line, on the other
  // hand, _is_ an object literal, followed by division.  Our old minifier gets
  // the second one wrong, but the new minifier should handle it correctly.
  CheckMinification("{foo: 123} / x /i;", "{foo:123}/ x /i;");
  CheckNewMinification("x={foo: 1} / x /i;", "x={foo:1}/x/i;");
}

// See http://github.com/apache/incubator-pagespeed-mod/issues/327
TEST_F(JsMinifyTest, RegexLiteralWithBrackets1) {
  // The / in [^/] doesn't end the regex, so the // is not a comment.
  CheckMinification("var x = /http:\\/\\/[^/]+\\//, y = 3;",
                    "var x=/http:\\/\\/[^/]+\\//,y=3;");
}

TEST_F(JsMinifyTest, RegexLiteralWithBrackets2) {
  // The first ] is escaped and doesn't close the [, so the following / doesn't
  // close the regex, so the following space is still in the regex and must be
  // preserved.
  CheckMinification("var x = /z[\\]/ ]/, y = 3;",
                    "var x=/z[\\]/ ]/,y=3;");
}

TEST_F(JsMinifyTest, ReturnRegex1) {
  // Make sure we understand that this is not division; "return" is not an
  // identifier!
  CheckMinification("return / x /g;", "return/ x /g;");
}

TEST_F(JsMinifyTest, ReturnRegex2) {
  // This test comes from the real world.  If "return" is incorrectly treated
  // as an identifier, the second slash will be treated as opening a regex
  // rather than closing it, and we'll error due to an unclosed regex.
  CheckMinification("return/#.+/.test(\n'#24' );",
                    "return/#.+/.test('#24');");
}

TEST_F(JsMinifyTest, ThrowRegex) {
  // Make sure we understand that this is not division; "throw" is not an
  // identifier!  (And yes, in JS you're allowed to throw a regex.)
  CheckMinification("throw / x /g;", "throw/ x /g;");
}

TEST_F(JsMinifyTest, ReturnThrowNumber) {
  CheckMinification("return 1;\nthrow 2;", "return 1;throw 2;");
}

TEST_F(JsMinifyTest, KeywordPrecedesRegex) {
  // Make sure "typeof /./" sees the first "/" as a regex and not division.
  // If it thinks it's a division then it will treat the "/    /" as a regex
  // and not remove the comment.  Do the same for all such keywords.
  // Example, "typeof /./    /* hi there */;" ->  "typeof/./;"
  for (pagespeed::JsKeywords::Iterator iter; !iter.AtEnd(); iter.Next()) {
    if (pagespeed::JsKeywords::CanKeywordPrecedeRegEx(iter.name())) {
      GoogleString input =
          net_instaweb::StrCat(iter.name(), " /./   /* hi there */;");
      GoogleString expected = net_instaweb::StrCat(iter.name(), "/./;");
      CheckMinification(input, expected);
    }
  }
}

TEST_F(JsMinifyTest, LoopRegex) {
  // Make sure we understand that a slash after "while (...)" or "for (...)" is
  // a regex, not division.  Our old minifier gets this wrong, but the new
  // minifier should handle it correctly.
  CheckNewMinification("while (0) /\\//.exec('');",
                       "while(0)/\\//.exec('');");
  CheckNewMinification("for (x in y) / z /.exec(x);",
                       "for(x in y)/ z /.exec(x);");
}

TEST_F(JsMinifyTest, LabelRegex) {
  // Make sure we understand that a slash after a label is a regex, not
  // division.
  CheckMinification("{ foo: / x /.exec(''); }", "{foo:/ x /.exec('');}");
}

const char kCrashTestString[] =
    "var x = 'asd \\' lse'\n"
    "var y /*comment*/ = /regex/\n"
    "var z = \"x =\" + x\n";

TEST_F(JsMinifyTest, DoNotCrash) {
  // Run on all possible prefixes of kCrashTestString.  We don't care about the
  // result; we just want to make sure it doesn't crash.
  for (int i = 0, size = sizeof(kCrashTestString); i <= size; ++i) {
    GoogleString input(kCrashTestString, i);
    GoogleString output;
    pagespeed::js::MinifyJs(input, &output);
  }
}

// The below tests check for some corner cases of semicolon insertion, to make
// sure that we are minifying as much as possible (and no more!).
// See http://inimino.org/~inimino/blog/javascript_semicolons for details.

TEST_F(JsMinifyTest, SemicolonInsertionIncrement) {
  CheckMinification("a\n++b\nc++\nd", "a\n++b\nc++\nd");
  // A trickier case that only the new minifier gets right:
  CheckNewMinification("a\n++\nb\nc++\nd", "a\n++b\nc++\nd");
}

TEST_F(JsMinifyTest, SemicolonInsertionDecrement) {
  CheckMinification("a\n--b\nc--\nd", "a\n--b\nc--\nd");
  // A trickier case that only the new minifier gets right:
  CheckNewMinification("a\n--\nb\nc--\nd", "a\n--b\nc--\nd");
}

TEST_F(JsMinifyTest, SemicolonInsertionAddition) {
  // No semicolons will be inserted, so the linebreaks can be removed.
  CheckMinification("i\n+\nj", "i+j");
}

TEST_F(JsMinifyTest, SemicolonInsertionSubtraction) {
  // No semicolons will be inserted, so the linebreaks can be removed.
  CheckMinification("i\n-\nj", "i-j");
}

TEST_F(JsMinifyTest, SemicolonInsertionLogicalOr) {
  // No semicolons will be inserted, so the linebreaks can be removed.
  CheckMinification("i\n||\nj", "i||j");
}

TEST_F(JsMinifyTest, SemicolonInsertionFuncCall) {
  // No semicolons will be inserted, so the linebreak can be removed.  This is
  // actually a function call, not two statements.
  CheckMinification("a = b + c\n(d + e).print()", "a=b+c(d+e).print()");
}

TEST_F(JsMinifyTest, SemicolonInsertionRegex) {
  // No semicolon will be inserted, so the linebreak and spaces can be removed
  // (this is two divisions, not a regex).
  CheckMinification("i=0\n/ [a-z] /g.exec(s)", "i=0/[a-z]/g.exec(s)");
}

TEST_F(JsMinifyTest, SemicolonInsertionComment) {
  CheckMinification("a=b\n /*hello*/ c=d\n", "a=b\nc=d");
}

TEST_F(JsMinifyTest, SemicolonInsertionWhileStmt) {
  // No semicolon will be inserted, so the linebreak can be removed.
  CheckMinification("while\n(true);", "while(true);");
}

TEST_F(JsMinifyTest, SemicolonInsertionReturnStmt1) {
  // A semicolon _will_ be inserted, so the linebreak _cannot_ be removed.
  CheckMinification("return\n(true);", "return\n(true);");
}

TEST_F(JsMinifyTest, SemicolonInsertionReturnStmt2) {
  // A semicolon _will_ be inserted, so the linebreak _cannot_ be removed.
  CheckMinification("return\n/*comment*/(true);", "return\n(true);");
}

TEST_F(JsMinifyTest, SemicolonInsertionThrowStmt) {
  // This is _not_ legal code; don't accidentally make it legal by removing the
  // linebreak.  (Eliminating a syntax error would change the semantics!)
  CheckMinification("throw\n  'error';", "throw\n'error';");
}

TEST_F(JsMinifyTest, SemicolonInsertionBreakStmt) {
  // A semicolon _will_ be inserted, so the linebreak _cannot_ be removed.
  CheckMinification("break\nlabel;", "break\nlabel;");
}

TEST_F(JsMinifyTest, SemicolonInsertionContinueStmt) {
  // A semicolon _will_ be inserted, so the linebreak _cannot_ be removed.
  CheckMinification("continue\nlabel;", "continue\nlabel;");
}

TEST_F(JsMinifyTest, SemicolonInsertionDebuggerStmt) {
  // A semicolon _will_ be inserted, so the linebreak _cannot_ be removed.
  CheckMinification("debugger\nfoo;", "debugger\nfoo;");
}

TEST_F(JsMinifyTest, Latin1Input) {
  // Try to minify input that is Latin-1 encoded.  This is not valid UTF-8, but
  // we should be able to proceed gracefully (in most cases) if the non-ascii
  // characters only ever appear in string literals and comments.
  CheckMinification("str='Qu\xE9 pasa';// 'qu\xE9' means 'what'\n"
                    "cents=/* 73\xA2 is $0.73 */73;",
                    "str='Qu\xE9 pasa';cents=73;");
}

const char kCollapsingStringTestString[] =
    "var x = 'asd \\' lse'\n"
    "var y /*comment*/ = /re'gex/\n"
    "var z = \"x =\" + x\n";

const char kCollapsedTestString[] =
    "var x=''\n"
    "var y=/re'gex/\n"
    "var z=\"\"+x";


TEST_F(JsMinifyTest, CollapsingStringTest) {
    int size = 0;
    GoogleString output;
    ASSERT_TRUE(pagespeed::js::MinifyJsAndCollapseStrings(
        kCollapsingStringTestString, &output));
    ASSERT_EQ(strlen(kCollapsedTestString), output.size());
    ASSERT_EQ(kCollapsedTestString, output);

    ASSERT_TRUE(pagespeed::js::GetMinifiedStringCollapsedJsSize(
        kCollapsingStringTestString, &size));
    ASSERT_EQ(static_cast<int>(strlen(kCollapsedTestString)), size);
}

TEST_F(JsMinifyTest, MinifyAngular) {
  CheckFileMinification("angular.original", "angular.minified");
}

TEST_F(JsMinifyTest, MinifyJQuery) {
  CheckFileMinification("jquery.original", "jquery.minified");
}

TEST_F(JsMinifyTest, MinifyPrototype) {
  CheckFileMinification("prototype.original", "prototype.minified");
}

// Simple method for serializing Mappings so that they can be compared against
// gold versions.
GoogleString MappingsToString(
    const net_instaweb::source_map::MappingVector& mappings) {
  GoogleString result("{");
  for (int i = 0, n = mappings.size(); i < n; ++i) {
    StrAppend(&result, "(",
              net_instaweb::IntegerToString(mappings[i].gen_line), ", ",
              net_instaweb::IntegerToString(mappings[i].gen_col),  ", ");
    StrAppend(&result,
              net_instaweb::IntegerToString(mappings[i].src_file), ", ",
              net_instaweb::IntegerToString(mappings[i].src_line), ", ",
              net_instaweb::IntegerToString(mappings[i].src_col),  "), ");
  }
  result += "}";
  return result;
}

TEST_F(JsMinifyTest, SourceMapsSimple) {
  const char js_before[] =
      "/* Simple hello world program. */\n"
      "alert( 'Hello, World!' );\n";
  const char expected_js_after[] =
      "alert('Hello, World!');";
  const char expected_map[] =
      "{"
      "(0, 0, 0, 1, 0), "    // alert(
      "(0, 6, 0, 1, 7), "    // 'Hello, World!'
      "(0, 21, 0, 1, 23), "  // );
      "}";

  GoogleString output;
  net_instaweb::source_map::MappingVector mappings;
  EXPECT_TRUE(pagespeed::js::MinifyUtf8JsWithSourceMap(
      &patterns_, js_before, &output, &mappings));

  EXPECT_EQ(expected_js_after, output);

  EXPECT_EQ(expected_map, MappingsToString(mappings));
}

TEST_F(JsMinifyTest, SourceMapsComplex) {
  GoogleString output;
  net_instaweb::source_map::MappingVector mappings;
  EXPECT_TRUE(pagespeed::js::MinifyUtf8JsWithSourceMap(
      &patterns_, kBeforeCompilation, &output, &mappings));

  EXPECT_EQ(kAfterCompilationNew, output);

  const char expected_map[] =
      "{"
      "(0, 0, 0, 14, 0), "     // var is
      "(0, 6, 0, 14, 7), "     // =
      "(0, 7, 0, 14, 9), "     // {

      "(0, 8, 0, 15, 4), "     // ie:
      "(0, 11, 0, 15, 13), "   // navigator.appName
      "(0, 28, 0, 15, 31), "   // ==
      "(0, 30, 0, 15, 34), "   // 'Microsoft Internet Explorer',

      "(0, 60, 0, 16, 4), "    // java:
      "(0, 65, 0, 16, 13), "   // navigator.javaEnabled(),

      "(0, 89, 0, 17, 4), "    // ns:
      "(0, 92, 0, 17, 13), "   // navigator.appName
      "(0, 109, 0, 17, 31), "  // ==
      "(0, 111, 0, 17, 34), "  // 'Netscape',

      "(0, 122, 0, 18, 4), "   // ua:
      "(0, 125, 0, 18, 13), "  // navigator.userAgent.toLowerCase(),

      "(0, 159, 0, 19, 4), "   // version:
      "(0, 167, 0, 19, 13), "  // parseFloat(navigator.appVersion.substr(21))
      "(0, 210, 0, 19, 57), "  // ||
      "(0, 212, 0, 20, 13), "  // parseFloat(navigator.appVersion),

      "(0, 245, 0, 21, 4), "   // win:
      "(0, 249, 0, 21, 13), "  // navigator.platform
      "(0, 267, 0, 21, 32), "  // ==
      "(0, 269, 0, 21, 35), "  // 'Win32'
      "(0, 276, 0, 22, 0), "   // }

      "(1, 0, 0, 23, 0), "     // is.mac
      "(1, 6, 0, 23, 7), "     // =
      "(1, 7, 0, 23, 9), "     // is.ua.indexOf('mac')
      "(1, 27, 0, 23, 30), "   // >=
      "(1, 29, 0, 23, 33), "   // 0;

      "(1, 31, 0, 24, 0), "    // if
      "(1, 33, 0, 24, 3), "    // (is.ua.indexOf('opera')
      "(1, 56, 0, 24, 27), "   // >=
      "(1, 58, 0, 24, 30), "   // 0)
      "(1, 60, 0, 24, 33), "   // {

      "(1, 61, 0, 25, 4), "    // is.ie
      "(1, 66, 0, 25, 10), "   // =
      "(1, 67, 0, 25, 12), "   // is.ns
      "(1, 72, 0, 25, 18), "   // =
      "(1, 73, 0, 25, 20), "   // false;

      "(1, 79, 0, 26, 4), "    // is.opera
      "(1, 87, 0, 26, 13), "   // =
      "(1, 88, 0, 26, 15), "   // true;
      "(1, 93, 0, 27, 0), "    // }

      "(1, 94, 0, 28, 0), "    // if
      "(1, 96, 0, 28, 3), "    // (is.ua.indexOf('gecko')
      "(1, 119, 0, 28, 27), "  // >=
      "(1, 121, 0, 28, 30), "  // 0)
      "(1, 123, 0, 28, 33), "  // {

      "(1, 124, 0, 29, 4), "   // is.ie
      "(1, 129, 0, 29, 10), "  // =
      "(1, 130, 0, 29, 12), "  // is.ns
      "(1, 135, 0, 29, 18), "  // =
      "(1, 136, 0, 29, 20), "  // false;

      "(1, 142, 0, 30, 4), "   // is.gecko
      "(1, 150, 0, 30, 13), "  // =
      "(1, 151, 0, 30, 15), "  // true;
      "(1, 156, 0, 31, 0), "   // }
      "}";

  EXPECT_EQ(expected_map, MappingsToString(mappings));
}

}  // namespace
