/**
 * 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.hadoop.yarn.server.nodemanager.nodelabels;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileContext;
import org.apache.hadoop.fs.FileUtil;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.test.GenericTestUtils;
import org.apache.hadoop.util.Shell;
import org.apache.hadoop.yarn.api.records.NodeAttribute;
import org.apache.hadoop.yarn.api.records.NodeAttributeType;
import org.apache.hadoop.yarn.conf.YarnConfiguration;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Set;
import java.util.HashSet;
import java.util.Iterator;
import java.util.concurrent.TimeoutException;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;

/**
 * Test cases for script based node attributes provider.
 */
public class TestScriptBasedNodeAttributesProvider {

  private static File testRootDir = new File("target",
      TestScriptBasedNodeAttributesProvider.class.getName() + "-localDir")
      .getAbsoluteFile();

  private final File nodeAttributeScript =
      new File(testRootDir, Shell.appendScriptExtension("attributeScript"));

  private ScriptBasedNodeAttributesProvider nodeAttributesProvider;

  @BeforeEach
  public void setup() {
    testRootDir.mkdirs();
    nodeAttributesProvider = new ScriptBasedNodeAttributesProvider();
  }

  @AfterEach
  public void tearDown() throws Exception {
    if (testRootDir.exists()) {
      FileContext.getLocalFSFileContext()
          .delete(new Path(testRootDir.getAbsolutePath()), true);
    }
    if (nodeAttributesProvider != null) {
      nodeAttributesProvider.stop();
    }
  }

  private Configuration getConfForNodeAttributeScript() {
    Configuration conf = new Configuration();
    conf.set(YarnConfiguration.NM_SCRIPT_BASED_NODE_ATTRIBUTES_PROVIDER_PATH,
        nodeAttributeScript.getAbsolutePath());
    // set bigger interval so that test cases can be run
    conf.setLong(
        YarnConfiguration.NM_NODE_ATTRIBUTES_PROVIDER_FETCH_INTERVAL_MS,
        1000);
    conf.setLong(
        YarnConfiguration.NM_NODE_ATTRIBUTES_PROVIDER_FETCH_TIMEOUT_MS,
        1000);
    return conf;
  }

  private void writeNodeAttributeScriptFile(String scriptStr,
      boolean setExecutable) throws IOException {
    PrintWriter pw = null;
    try {
      FileUtil.setWritable(nodeAttributeScript, true);
      FileUtil.setReadable(nodeAttributeScript, true);
      pw = new PrintWriter(new FileOutputStream(nodeAttributeScript));
      pw.println(scriptStr);
      pw.flush();
    } catch (Exception e) {
      e.printStackTrace();
      fail();
    } finally {
      if (null != pw) {
        pw.close();
      }
    }
    FileUtil.setExecutable(nodeAttributeScript, setExecutable);
  }

  @Test
  public void testNodeAttributeScriptProvider()
      throws IOException, InterruptedException {
    String simpleScript = "echo NODE_ATTRIBUTE:host,STRING,host1234\n "
        + "echo NODE_ATTRIBUTE:os,STRING,redhat_6_3\n "
        + "echo NODE_ATTRIBUTE:ip,STRING,10.0.0.1";
    writeNodeAttributeScriptFile(simpleScript, true);

    nodeAttributesProvider.init(getConfForNodeAttributeScript());
    nodeAttributesProvider.start();

    try {
      GenericTestUtils.waitFor(
          () -> nodeAttributesProvider.getDescriptors().size() == 3,
          500, 3000);
    } catch (TimeoutException e) {
      fail("Expecting node attributes size is 3, but got "
          + nodeAttributesProvider.getDescriptors().size());
    }

    Iterator<NodeAttribute> it = nodeAttributesProvider
        .getDescriptors().iterator();
    while (it.hasNext()) {
      NodeAttribute att = it.next();
      switch (att.getAttributeKey().getAttributeName()) {
      case "host":
        assertEquals(NodeAttributeType.STRING, att.getAttributeType());
        assertEquals("host1234", att.getAttributeValue());
        break;
      case "os":
        assertEquals(NodeAttributeType.STRING, att.getAttributeType());
        assertEquals("redhat_6_3", att.getAttributeValue());
        break;
      case "ip":
        assertEquals(NodeAttributeType.STRING, att.getAttributeType());
        assertEquals("10.0.0.1", att.getAttributeValue());
        break;
      default:
        fail("Unexpected attribute name "
            + att.getAttributeKey().getAttributeName());
        break;
      }
    }
  }

  @Test
  public void testInvalidScriptOutput()
      throws IOException, InterruptedException {
    // Script output doesn't have correct prefix.
    String scriptContent = "echo host,STRING,host1234";
    writeNodeAttributeScriptFile(scriptContent, true);

    nodeAttributesProvider.init(getConfForNodeAttributeScript());
    nodeAttributesProvider.start();

    try {
      GenericTestUtils.waitFor(
          () -> nodeAttributesProvider.getDescriptors().size() == 1,
          500, 3000);
      fail("This test should timeout because the provide is unable"
          + " to parse any attributes from the script output.");
    } catch (TimeoutException e) {
      assertEquals(0, nodeAttributesProvider
          .getDescriptors().size());
    }
  }

  @Test
  public void testMalformedScriptOutput() throws Exception{
    // Script output has correct prefix but each line is malformed.
    String scriptContent =
        "echo NODE_ATTRIBUTE:host,STRING,host1234,a_extra_column";
    writeNodeAttributeScriptFile(scriptContent, true);

    nodeAttributesProvider.init(getConfForNodeAttributeScript());
    nodeAttributesProvider.start();

    // There should be no attributes found, and we should
    // see Malformed output warnings in the log
    try {
      GenericTestUtils
          .waitFor(() -> nodeAttributesProvider
                  .getDescriptors().size() == 1,
              500, 3000);
      fail("This test should timeout because the provide is unable"
          + " to parse any attributes from the script output.");
    } catch (TimeoutException e) {
      assertEquals(0, nodeAttributesProvider
          .getDescriptors().size());
    }
  }

  @Test
  public void testFetchInterval() throws Exception {
    // The script returns the pid (as an attribute) each time runs this script
    String simpleScript = "echo NODE_ATTRIBUTE:pid,STRING,$$";
    writeNodeAttributeScriptFile(simpleScript, true);

    nodeAttributesProvider.init(getConfForNodeAttributeScript());
    nodeAttributesProvider.start();

    // Wait for at most 3 seconds until we get at least 1
    // different attribute value.
    Set<String> resultSet = new HashSet<>();
    GenericTestUtils.waitFor(() -> {
      Set<NodeAttribute> attributes =
          nodeAttributesProvider.getDescriptors();
      if (attributes != null) {
        assertEquals(1, attributes.size());
        resultSet.add(attributes.iterator().next().getAttributeValue());
        return resultSet.size() > 1;
      } else {
        return false;
      }
    }, 500, 3000);
  }

  @Test
  public void testNodeAttributesValidation() throws Exception{
    // Script output contains ambiguous node attributes
    String scriptContent = "echo NODE_ATTRIBUTE:host,STRING,host1234\n "
        + "echo NODE_ATTRIBUTE:host,STRING,host2345\n "
        + "echo NODE_ATTRIBUTE:ip,STRING,10.0.0.1";

    writeNodeAttributeScriptFile(scriptContent, true);

    nodeAttributesProvider.init(getConfForNodeAttributeScript());
    nodeAttributesProvider.start();

    // There should be no attributes found, and we should
    // see Malformed output warnings in the log
    try {
      GenericTestUtils
          .waitFor(() -> nodeAttributesProvider
                  .getDescriptors().size() == 3,
              500, 3000);
      fail("This test should timeout because the provide is unable"
          + " to parse any attributes from the script output.");
    } catch (TimeoutException e) {
      assertEquals(0, nodeAttributesProvider
          .getDescriptors().size());
    }
  }
}
