content: /*
   * 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.
   */
  package org.apache.calcite.adapter.os;
  
  import org.apache.calcite.config.CalciteConnectionProperty;
  import org.apache.calcite.config.Lex;
  import org.apache.calcite.runtime.Hook;
  import org.apache.calcite.sql.validate.SqlConformanceEnum;
  import org.apache.calcite.test.CalciteAssert;
  import org.apache.calcite.util.Holder;
  import org.apache.calcite.util.Sources;
  import org.apache.calcite.util.TestUtil;
  import org.apache.calcite.util.Util;
  
  import org.junit.jupiter.api.Test;
  
  import java.io.ByteArrayInputStream;
  import java.io.File;
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.InputStreamReader;
  import java.io.OutputStream;
  import java.io.PrintWriter;
  import java.io.StringWriter;
  import java.net.URL;
  import java.nio.charset.StandardCharsets;
  import java.sql.SQLException;
  import java.util.function.Consumer;
  
  import static org.apache.calcite.util.TestUtil.rethrow;
  
  import static org.hamcrest.CoreMatchers.any;
  import static org.hamcrest.CoreMatchers.is;
  import static org.hamcrest.CoreMatchers.notNullValue;
  import static org.hamcrest.CoreMatchers.startsWith;
  import static org.hamcrest.MatcherAssert.assertThat;
  import static org.hamcrest.Matchers.hasToString;
  import static org.junit.jupiter.api.Assertions.assertNotNull;
  import static org.junit.jupiter.api.Assertions.fail;
  import static org.junit.jupiter.api.Assumptions.assumeFalse;
  import static org.junit.jupiter.api.Assumptions.assumeTrue;
  
  import static java.util.Objects.requireNonNull;
  
  /**
   * Unit tests for the OS (operating system) adapter.
   *
   * <p>Also please run the following tests manually, from your shell:
   *
   * <ul>
   *   <li>./sqlsh select \* from du
   *   <li>./sqlsh select \* from files
   *   <li>./sqlsh select \* from git_commits
   *   <li>./sqlsh select \* from ps
   *   <li>(echo cats; echo and dogs) | ./sqlsh select \* from stdin
   *   <li>./sqlsh select \* from vmstat
   * </ul>
   */
  class OsAdapterTest {
    private static final String OS_NAME = System.getProperty("os.name");
  
    private static boolean isWindows() {
      return OS_NAME.startsWith("Windows");
    }
  
    /** Returns whether there is a ".git" directory in this directory or in a
     * directory between this directory and root. */
    private static boolean hasGit() {
      assumeToolExists("git");
      final URL url = requireNonNull(OsAdapterTest.class.getResource("/"));
      final String path = Sources.of(url).file().getAbsolutePath();
      File f = new File(path);
      for (;;) {
        if (f == null || !f.exists()) {
          return false; // abandon hope
        }
        File[] files =
            f.listFiles((dir, name) -> name.equals(".git"));
        if (files != null && files.length == 1) {
          return true; // there is a ".git" subdirectory
        }
        f = f.getParentFile();
      }
    }
  
    private static void assumeToolExists(String command) {
      assumeTrue(checkProcessExists(command), () -> command + " does not exist");
    }
  
    private static boolean checkProcessExists(String command) {
      try {
        Process process = new ProcessBuilder().command(command).start();
        assertNotNull(process);
        int errCode = process.waitFor();
        assertThat(errCode, is(0));
        return true;
      } catch (AssertionError | IOException | InterruptedException e) {
        return false;
      }
    }
  
    @Test void testDu() {
      assumeFalse(isWindows(), "Skip: the 'du' table does not work on Windows");
      assumeToolExists("du");
      sql("select * from du")
          .returns(r -> {
            try {
              assertThat(r.next(), is(true));
              assertThat(r.getInt(1), notNullValue());
              assertThat(r.getString(2), startsWith("./"));
              assertThat(r.wasNull(), is(false));
            } catch (SQLException e) {
              throw rethrow(e);
            }
          });
    }
  
    @Test void testDuFilterSortLimit() {
      assumeFalse(isWindows(), "Skip: the 'du' table does not work on Windows");
      assumeToolExists("du");
      sql("select * from du where path like '%/src/test/java/%'\n"
          + "order by 1 limit 2")
          .returns(r -> {
            try {
              assertThat(r.next(), is(true));
              assertThat(r.getInt(1), notNullValue());
              assertThat(r.getString(2), startsWith("./"));
              assertThat(r.wasNull(), is(false));
              assertThat(r.next(), is(true));
              assertThat(r.next(), is(false)); // because of "limit 2"
            } catch (SQLException e) {
              throw rethrow(e);
            }
          });
    }
  
    @Test void testFiles() {
      assumeFalse(isWindows(), "Skip: the 'files' table does not work on Windows");
      sql("select distinct type from files")
          .returnsUnordered("type=d",
              "type=f");
    }
  
    @Test void testPs() {
      assumeFalse(isWindows(), "Skip: the 'ps' table does not work on Windows");
      assumeToolExists("ps");
      sql("select * from ps")
          .returns(r -> {
            try {
              assertThat(r.next(), is(true));
              final StringBuilder b = new StringBuilder();
              final int c = r.getMetaData().getColumnCount();
              for (int i = 0; i < c; i++) {
                b.append(r.getString(i + 1)).append(';');
                assertThat(r.wasNull(), is(false));
              }
              assertThat(b, hasToString(notNullValue()));
            } catch (SQLException e) {
              throw rethrow(e);
            }
          });
    }
  
    @Test void testPsDistinct() {
      assumeFalse(isWindows(), "Skip: the 'ps' table does not work on Windows");
      assumeToolExists("ps");
      sql("select distinct `user` from ps")
          .returns(r -> {
            try {
              assertThat(r.next(), is(true));
              assertThat(r.getString(1), notNullValue());
              assertThat(r.wasNull(), is(false));
            } catch (SQLException e) {
              throw rethrow(e);
            }
          });
    }
  
    @Test void testGitCommits() {
      assumeTrue(hasGit(), "no git");
      sql("select count(*) from git_commits")
          .returns(r -> {
            try {
              assertThat(r.next(), is(true));
              assertThat(r.getString(1), notNullValue());
              assertThat(r.wasNull(), is(false));
            } catch (SQLException e) {
              throw TestUtil.rethrow(e);
            }
          });
    }
  
    @Test void testGitCommitsTop() {
      assumeTrue(hasGit(), "no git");
      final String q = "select author from git_commits\n"
          + "group by 1 order by count(*) desc limit 2";
      sql(q).returnsUnordered("author=Julian Hyde <julianhyde@gmail.com>",
          "author=Julian Hyde <jhyde@apache.org>");
    }
  
    @Test void testJps() {
      assumeToolExists("jps");
      final String q = "select pid, info from jps";
      sql(q).returns(r -> {
        try {
          if (r.next()) {
            assertThat(r.getString(1), notNullValue());
            assertThat(r.getString(2), notNullValue());
            assertThat(r.wasNull(), is(false));
          }
        } catch (SQLException e) {
          throw TestUtil.rethrow(e);
        }
      });
    }
  
    @Test void testVmstat() {
      assumeFalse(isWindows(), "Skip: the 'files' table does not work on Windows");
      assumeToolExists("vmstat");
      sql("select * from vmstat")
          .returns(r -> {
            try {
              assertThat(r.next(), is(true));
              final int c = r.getMetaData().getColumnCount();
              for (int i = 0; i < c; i++) {
                assertThat(r.getLong(i + 1), notNullValue());
                assertThat(r.wasNull(), is(false));
              }
            } catch (SQLException e) {
              throw rethrow(e);
            }
          });
    }
  
    @Test void testStdin() throws SQLException {
      try (Hook.Closeable ignore =
          Hook.STANDARD_STREAMS.addThread((Consumer<Holder<Object[]>>) o -> {
            final Object[] values = o.get();
            final InputStream in = (InputStream) values[0];
            final String s = "First line\n"
                + "Second line";
            final ByteArrayInputStream in2 =
                new ByteArrayInputStream(s.getBytes(StandardCharsets.UTF_8));
            final OutputStream out = (OutputStream) values[1];
            final OutputStream err = (OutputStream) values[2];
            o.set(new Object[] {in2, out, err});
          })) {
        assertThat(foo("select count(*) as c from stdin"), is("2\n"));
      }
    }
  
    @Test void testStdinExplain() {
      // Can't execute stdin, because junit's stdin never ends;
      // so just run explain
      final String explain = "PLAN="
          + "EnumerableAggregate(group=[{}], c=[COUNT()])\n"
          + "  EnumerableTableFunctionScan(invocation=[stdin(true)], "
          + "rowType=[RecordType(INTEGER ordinal, VARCHAR line)], "
          + "elementType=[class [Ljava.lang.Object;])";
      sql("select count(*) as c from stdin")
          .explainContains(explain);
    }
  
    @Test void testSqlShellFormat() throws SQLException {
      final String q = "select * from (values (-1, true, 'a'),"
          + " (2, false, 'b, c'),"
          + " (3, unknown, cast(null as char(1)))) as t(x, y, z)";
      final String empty = q + " where false";
  
      final String spacedOut = "-1 true a   \n"
          + "2 false b, c\n"
          + "3 null null\n";
      assertThat(foo("-o", "spaced", q), is(spacedOut));
  
      assertThat(foo("-o", "spaced", empty), is(""));
  
      // default is 'spaced'
      assertThat(foo(q), is(spacedOut));
  
      final String headersOut = "x y z\n"
          + spacedOut;
      assertThat(foo("-o", "headers", q), is(headersOut));
  
      final String headersEmptyOut = "x y z\n";
      assertThat(foo("-o", "headers", empty), is(headersEmptyOut));
  
      final String jsonOut = "[\n"
          + "{\n"
          + "  \"x\": -1,\n"
          + "  \"y\": true,\n"
          + "  \"z\": \"a   \"\n"
          + "},\n"
          + "{\n"
          + "  \"x\": 2,\n"
          + "  \"y\": false,\n"
          + "  \"z\": \"b, c\"\n"
          + "},\n"
          + "{\n"
          + "  \"x\": 3,\n"
          + "  \"y\": null,\n"
          + "  \"z\": null\n"
          + "}\n"
          + "]\n";
      assertThat(foo("-o", "json", q), is(jsonOut));
  
      final String jsonEmptyOut = "[\n"
          + "]\n";
      assertThat(foo("-o", "json", empty), is(jsonEmptyOut));
  
      final String csvEmptyOut = "[\n"
          + "]\n";
      assertThat(foo("-o", "json", empty), is(csvEmptyOut));
  
      final String csvOut = "x,y,z\n"
          + "-1,true,a   \n"
          + "2,false,\"b, c\"\n"
          + "3,,";
      assertThat(foo("-o", "csv", q), is(csvOut));
  
      final String mysqlOut = ""
          + "+----+-------+------+\n"
          + "|  x | y     | z    |\n"
          + "+----+-------+------+\n"
          + "| -1 | true  | a    |\n"
          + "|  2 | false | b, c |\n"
          + "|  3 |       |      |\n"
          + "+----+-------+------+\n"
          + "(3 rows)\n"
          + "\n";
      assertThat(foo("-o", "mysql", q), is(mysqlOut));
  
      final String mysqlEmptyOut = ""
          + "+---+---+---+\n"
          + "| x | y | z |\n"
          + "+---+---+---+\n"
          + "+---+---+---+\n"
          + "(0 rows)\n"
          + "\n";
      assertThat(foo("-o", "mysql", empty), is(mysqlEmptyOut));
    }
  
    private String foo(String... args) throws SQLException {
      final ByteArrayInputStream inStream = new ByteArrayInputStream(new byte[0]);
      final InputStreamReader in =
          new InputStreamReader(inStream, StandardCharsets.UTF_8);
      final StringWriter outSw = new StringWriter();
      final PrintWriter out = new PrintWriter(outSw);
      final StringWriter errSw = new StringWriter();
      final PrintWriter err = new PrintWriter(errSw);
      new SqlShell(in, out, err, args).run();
      return Util.toLinux(outSw.toString());
    }
  
    @Test void testSqlShellHelp() throws SQLException {
      final String help = "Usage: sqlsh [OPTION]... SQL\n"
          + "Execute a SQL command\n"
          + "\n"
          + "Options:\n"
          + "  -o FORMAT  Print output in FORMAT; options are 'spaced' (the "
          + "default), 'csv',\n"
          + "             'headers', 'json', 'mysql'\n"
          + "  -h --help  Print this help\n";
      final String q = "select 1";
      assertThat(foo("--help", q), is(help));
  
      assertThat(foo("-h", q), is(help));
  
      try {
        final String s = foo("-o", "bad", q);
        fail("expected exception, got " + s);
      } catch (RuntimeException e) {
        assertThat(e.getMessage(), is("unknown format: bad"));
      }
    }
  
    static CalciteAssert.AssertQuery sql(String sql) {
      return CalciteAssert.that()
          .withModel(SqlShell.MODEL)
          .with(CalciteConnectionProperty.LEX, Lex.JAVA)
          .with(CalciteConnectionProperty.CONFORMANCE, SqlConformanceEnum.LENIENT)
          .query(sql);
    }
  
    private void checkOsQuery(String tableName, int colSize) {
      final String q = "select * from " + tableName;
      sql(q).returns(r -> {
        try {
          if (r.next()) {
            for (int i = 1; i <= colSize; i++) {
              if (r.getString(i) != null) {
                assertThat(r.getString(i), any(String.class));
              }
            }
            assertThat(r.wasNull(), is(false));
          }
        } catch (SQLException e) {
          throw TestUtil.rethrow(e);
        }
      });
    }
  
    @Test void testSystemInfo() {
      checkOsQuery("system_info", 18);
    }
  
    @Test void testJavaInfo() {
      checkOsQuery("java_info", 18);
    }
  
    @Test void testCpuInfo() {
      checkOsQuery("cpu_info", 9);
    }
  
    @Test void testCpuTime() {
      checkOsQuery("cpu_time", 8);
    }
  
    @Test void testOsVersion() {
      checkOsQuery("os_version", 5);
    }
  
    @Test void testMemoryInfo() {
      checkOsQuery("memory_info", 8);
    }
  
    @Test void testInterfaceAddresses() {
      checkOsQuery("interface_addresses", 5);
    }
  
    @Test void testInterfaceDetails() {
      checkOsQuery("interface_details", 13);
    }
  
    @Test void testMounts() {
      checkOsQuery("mounts", 8);
    }
  }
  