# Title: z_a_TestCases_CheckOutput_WeatherPrep.py
# 	Description: This program is designed to test WeatherPrep modifications and ensure that deliberate changes have not introduced bugs. 
#	The program calls WeatherPrep.exe to test if it will generate output identical to the expected output for known input of test cases. 
#
# Software Requirements: Python 3.6 64-bit
# Input: No command line argument, simply a file in the same directory as the Python script.
#   File: ListOfTests.txt: This file should be placed in the same directory as the Python script, within content below:
#       Executable line: "Executable" key word followed by "," and then path to latest build of WeatherPrep.exe; path relative to local directory.
#       Test header line: "TestName" key word followed by "," and then "TestCaseFolderPath" key word; TestName is fixed and searched for in algorithm below
#       Next lines: 1 line for each test with a value for TestName and TestCaseFolderPath ending in backslash, separated by commas
#       TestName = arbitrary name without white space that becomes name of folder holding testing results. 
#       TestCaseFolderPath = path to the known test case folder, with internal and populated folders for input and expectedoutput
#
#   Example ListOfTests.txt file content:
#       Executable,..\WeatherPrep\Release\WeatherPrep.exe
#       TestName,TestCaseFolderPathWithEndingBackslash
#       Hydro_Statistical_Exp_TI,TestCases\StatHydro\expIR_defaultParams\
#
# Output: Placed in folder labeled Testing_%Y_%m_%d_%H_%M_%S folder created in same directory where Python script was launched
#   File: TestResults.txt file details of each test outcome (Fail or Pass) and summary of all tests. 
#   Sub-Folders: TestName folders for each test, with input, output, and expectedoutput folders filled with content
#
# How to Run: 
#	Open Windows Command Prompt, go to directory with Python script, e.g.,C:\>cd C:\iTree\WeatherPrep\TestingFilesAndScript. 
#	C:\iTree\WeatherPrep\TestingFilesAndScript>python z_a_TestCases_CheckOutput_WeatherPrep.py
# 		
# Created: i-Tree coding employee Shannon Conley, 2018 while working to implement standard operating procedure algorithms of with R. Coville and T. Endreny
# Updated:
# 2019/10/10:   T. Endreny, Updated directions, commented all code, refactored to read latest executable file from ListOfTests.txt rather than take as CLI 
# 2021/11/05:   T. Endreny, Updated directions, clarified variable names, refactored to simplify flow using single input path, removed use with Hydro GUI, reformatted all indentations
# 2023/02/19:   T. Endreny, Updated string structure of input paths and parsing of input and output directories to allow more flexibility 
# ========================================================================================================================================================

#import Python libraries
import subprocess
import os
import pandas as pd
import time
from random import uniform
import numpy as np
import csv 
import shutil
import glob
from filecmp import dircmp
#lxml.etree is an XML reader that will preserve comments in the XML
try:
    import lxml.etree as XML_eTree_File
except ImportError:
    print("Warning: The Python XML parsing library 'lxml' is not installed on this machine. It is needed to run this Python script.\n")
    print("Solution: Please install the XML parsing library 'lxml' using one of the following commands, executed from the Anaconda or DOS XTerm with administrator privileges:\n")
    print("conda install -c anaconda lxml")
    print("pip install lxml")
    sys.exit()

#define input and expectedoutput folders containing TestCase files
#Input_Folder_name defined as empty, to accommodate test cases such as multi-quad using quad00# to hold inputs 
#Note: Input_Folder_name is defined below, read from the input string which includes it after the final backslash
folder_expectedoutput = "expectedoutput"
#define ListOfTests_file_name as input ListOfTests.txt
ListOfTests_file_name = "ListOfTests.txt"

#TimeDate_StartProcessingSimulation_seconds is timer at start of simulation
TimeDate_StartProcessingSimulation_seconds = time.time()

#Note: modify_params not called for WeatherPrepConfig.xml, but function remains in case needed in future. 
#modify_params function called from below to update WeatherPrepConfig.xml
#   Values passed for variable names path_to_TestCase_WeatherPrepConfgXml, Xml_Key_Elements_String, and path_to_Testing_OuputFolder
def modify_params(path_to_TestCase_WeatherPrepConfgXml, Xml_Key_Elements_String, path_to_Testing_OuputFolder):
	#define XML_eTree_File_Tree as parsed set of keys from WeatherPrepConfig.xml file
    #Note: This parse function drops all comments in the WeatherPrepConfig.xml file
    #Note: This loss then propagates to TestCases expectedresults folders after using z_Copy_TestCaseOutput_to_ExpectedOutput.py
    XML_eTree_File_Tree = XML_eTree_File.parse(path_to_TestCase_WeatherPrepConfgXml)
	#define XML_eTree_File_Element the value or path of the Xml_Key_Elements_String <OutputDirectory> within WeatherPrepConfig.xml file
    XML_eTree_File_Element = XML_eTree_File_Tree.find(Xml_Key_Elements_String)
	#redefine XML_eTree_File_Element to new value, path_to_Testing_OuputFolder, within HydroPlusConfg.xml file
    XML_eTree_File_Element.text = str(path_to_Testing_OuputFolder)
	#write XML_eTree_File_Tree as new WeatherPrepConfig.xml file in folder defined by path_to_TestCase_WeatherPrepConfgXml
    XML_eTree_File_Tree.write(path_to_TestCase_WeatherPrepConfgXml)

#open ListOfTests_file_name as ListOfTests_file
with open(ListOfTests_file_name) as ListOfTests_file:
	#define data for each line in ListOfTests_file with tuple command, reading objects separated by commas
    data=[tuple(line) for line in csv.reader(ListOfTests_file)]
	
#define Testing_DateTimeFolder_name folder as Testing_%Y_%m_%d_%H_%M_%S, with variables for time and date components given by strftime() method
Testing_DateTimeFolder_name = time.strftime("Testing_%Y_%m_%d_%H_%M_%S")

#define ListsOfTests_Results_file_name as TestResults.txt within Testing_DateTimeFolder_name folder
ListsOfTests_Results_file_name = Testing_DateTimeFolder_name + "\\TestResults.txt"

#define ListsOfTests_Results_string as strings to keep track of results
ListsOfTests_Results_string = {"Overall Test Results": "Passed"}

#for each row of data in ListOfTests.txt file
for ListOfTests_Input_Row in data:
    #if 1st element of row is Executable key word, then read in path to WeatherPrep.exe file
    if ListOfTests_Input_Row[0] == "Executable":
        #name WeatherPrep executable
        HydroPlusExe_path = ListOfTests_Input_Row[1]
        #Continue to next row of data, and return to for loop 
        continue
    #if 1st element of row is TestName key word, then this row is the Header row above the TestCase folder, WeatherPrep key word, and Testing folder data
    if ListOfTests_Input_Row[0] == "TestName":
        #Continue to next row of data
        continue
        
    #define Testing_CaseFolder_name within as 1st variable in data ListOfTests_Input_Row, counting starts at 0
    Testing_CaseFolder_name = ListOfTests_Input_Row[0]

    #define TestCase_Folder_path as value of 2nd element in ListOfTests_Input_Row, counting starts at 0
    TestCase_Folder_path = ListOfTests_Input_Row[1]

    #Note: Algorithm to use Input and Output folders was not needed at development, but is kept in case needed later
    # #Algorithm to parse TestCase_Folder_path for use with input and expectedoutput folders
    # #Find final backslash in TestCase_Folder_path string, which is point where input folder is listed
    # str_backslash = "\\"
    # #TestCase_Folder_w_Input_path defined as 2nd input ListOfTests_Input_Row, TestCase_Folder_path
    # TestCase_Folder_w_Input_path = TestCase_Folder_path
    # #len_backslash_inputs is length to backslash in TestCase_Folder_w_Input_path using .rfind function
    # len_backslash_inputs = TestCase_Folder_w_Input_path.rfind(str_backslash)
    # #len_TestCase_Folder_w_Input_path is length of string TestCase_Folder_w_Input_path
    # len_TestCase_Folder_w_Input_path = len(TestCase_Folder_w_Input_path)
    # #len_Input_Folder_name is length of string input folder
    # len_Input_Folder_name = (len_TestCase_Folder_w_Input_path - len_backslash_inputs)
    # #len_TestCase_Folder_path is len_backslash_inputs plus 1
    # len_TestCase_Folder_path = len_backslash_inputs + 1
    # #Input_Folder_name read from right in TestCase_Folder_w_Input_path distance -len_Input_Folder_name:
    # Input_Folder_name = TestCase_Folder_w_Input_path[-len_Input_Folder_name:]
    # #TestCase_Folder_path read from left in TestCase_Folder_path distance len_TestCase_Folder_path
    # TestCase_Folder_path = TestCase_Folder_path[:len_TestCase_Folder_path]
    # #Note: Overwriting above algorithm:
    # str_backslash = ""
    Input_Folder_name = ""
    
    #print("TestCase_Folder_w_Input_path is {}, final folder at {}".format(TestCase_Folder_w_Input_path, len_backslash_inputs))
    #print("Input_Folder_name is {}, len_TestCase_Folder_w_Input_path length {}".format(Input_Folder_name, len_TestCase_Folder_w_Input_path))
    #print("TestCase_Folder_path is {}".format(TestCase_Folder_path))

    #define Testing_DateTimeFolder_CaseFolder_names as combined string Testing_DateTimeFolder_name folder and Testing_CaseFolder_name, the unique test case name 
    Testing_DateTimeFolder_CaseFolder_names = time.strftime(Testing_DateTimeFolder_name + "\\" + Testing_CaseFolder_name)
    #define Testing_Output_path within Testing_DateTimeFolder_CaseFolder_names folder with name for output
    #Note: When a module is loaded from a file in Python, __file__ is set to its path.
    #Note: Algorithm to use Input and Output folders was not needed at development, but is kept in case needed later
    #Testing_Output_path = os.path.join(os.path.dirname(__file__),Testing_DateTimeFolder_CaseFolder_names + '\\output')
    Testing_Output_path = os.path.join(os.path.dirname(__file__),Testing_DateTimeFolder_CaseFolder_names)
    #define Testing_Input_path within Testing_DateTimeFolder_CaseFolder_names folder with name for input
    #Note: Algorithm to use Input and Output folders was not needed at development, but is kept in case needed later
    #Testing_Input_path = os.path.join(os.path.dirname(__file__),Testing_DateTimeFolder_CaseFolder_names + '\\input')
    Testing_Input_path = os.path.join(os.path.dirname(__file__),Testing_DateTimeFolder_CaseFolder_names)
    #define Testing_ExpectedOutput_path within Testing_DateTimeFolder_CaseFolder_names folder with name for expectedoutput
    Testing_ExpectedOutput_path = os.path.join(os.path.dirname(__file__),Testing_DateTimeFolder_CaseFolder_names + '\\expectedoutput')
     
    #make Testing_Output_path if it does not exit
    if not os.path.exists(Testing_Output_path):
        os.makedirs(Testing_Output_path)
    #make Testing_Input_path if it does not exit
    if not os.path.exists(Testing_Input_path):
        os.makedirs(Testing_Input_path)
    #make Testing_ExpectedOutput_path if it does not exit
    if not os.path.exists(Testing_ExpectedOutput_path):
        os.makedirs(Testing_ExpectedOutput_path)
		
    #define TestCase_Input_path as input path, 2nd variable in data ListOfTests_Input_Row, as upper case string
    TestCase_Input_path = TestCase_Folder_path + Input_Folder_name
        
    #for each file in the input path +/*.*, finding all file paths with glob command
    for file in glob.glob(TestCase_Input_path+'/*.*'):
        #copy file to Testing_Input_path directory
        shutil.copy(file, Testing_Input_path)

    #define TestCase_ExpectedOutput_path as expectedoutput path, adding backslash with extra escape character 
    TestCase_ExpectedOutput_path = TestCase_Folder_path + '\\' + folder_expectedoutput
        
    #for each file in the expectedoutput path +/*.*, finding all file paths with glob command
    for file in glob.glob(TestCase_ExpectedOutput_path+'/*.*'):     
        #copy file to newinpath directory
        shutil.copy(file, Testing_ExpectedOutput_path)

    #copy TestCase WeatherPrepConfig.xml to Testing folder
    #shutil.copy function transfers elements on left to elements on right within {} 
    shutil.copy(TestCase_Input_path + '\\WeatherPrepConfig.xml', Testing_Input_path + '\\WeatherPrepConfig.xml')

    #-------------------------------------------------------------------------------------------------------------------------
    #Replace elements within WeatherPrepConfig.xml file to swap out preserved TestCase with the new Testing case
    #-------------------------------------------------------------------------------------------------------------------------
    #call modify_params function to give path for Testing input directory and output path within WeatherPrepConfig.xml file 
    #cte 202302 Turning off modification; could parse string and rewrite path to input if needed
    #modify_params(Testing_Input_path+"\\WeatherPrepConfig.xml","Input/SurfaceWeatherDataFile", Testing_Output_path+"\\")

    #-------------------------------------------------------------------------------------------------------------------------
    #Command Line Input to call WeatherPrep.exe using Python function subprocess.call
    #-------------------------------------------------------------------------------------------------------------------------
    subprocess.call([HydroPlusExe_path,Testing_Input_path])
    #print the WeatherPrep command line argument
    print('{} {}'.format(HydroPlusExe_path,Testing_Input_path))

   
    #pause program for 1 sec, to complete above call
    time.sleep(1)
        
    # #cte 20210325 If Testing_Output_path is empty, rerun the model doesn't fix problem of empty directory
    # #initiate counter of runs
    # i_cnt_run = 0
    # #if Testing_Output_path directory is empty, and i_cnt_run < 3, call call the model again
    # while not os.listdir(Testing_Output_path) and i_cnt_run < 3:
    # #if not os.listdir(Testing_Output_path):
        # #advance the counter
        # i_cnt_run = i_cnt_run + 1
        # #write to screen the status of Testing_Output_path
        # print('OutputDirectory {} is empty, so model will run again.'.format(Testing_Output_path))
        # #call WeatherPrep.exe newinpath; this runs model with .xml and inputs in newinpath
        # subprocess.call([HydroPlusExe_path,newinpath])
        # #pause program for 1 sec, to complete above call
        # time.sleep(3)

    #-------------------------------------------------------------------------------------------------------------------------
    #Conduct test of differences between expected outputs vs outputs 
    #-------------------------------------------------------------------------------------------------------------------------
    #dircmp, a Python function for direct comparison between Testing_Output_path and Testing_ExpectedOutput_path files
    Expected_vs_Output_Difference_Test_Result = dircmp(Testing_Output_path, Testing_ExpectedOutput_path)
	
    #-------------------------------------------------------------------------------------------------------------------------
    #Write test result file
    #-------------------------------------------------------------------------------------------------------------------------
    #Open ListsOfTests_Results_file_name TestResults.txt as ListOfTests_file
    with open(ListsOfTests_Results_file_name,'a') as ListsOfTests_Results_file:

        #Write to string the Folder name and test result = Passed
        ListsOfTests_Results_string[ListOfTests_Input_Row[0]] =  "Passed"

        #Write to file the ListOfTests_Input_Row name
        ListsOfTests_Results_file.write("Test Folder name: " + Testing_CaseFolder_name + "\n")
        #Write to file the path to new output
        ListsOfTests_Results_file.write("Path of new output: " + Testing_Output_path + "\n")
        #Write to file the path to expected output
        ListsOfTests_Results_file.write("Path of expected output: " + Testing_ExpectedOutput_path + "\n")
        
        #-------------------------------------------------------------------------------------------------------------------------
        #Loop through differences between expected output and output
        #-------------------------------------------------------------------------------------------------------------------------
        #counter_missing initialized to zero
        counter_missing = 0
        #for each instance of the right_list, expectedoutput, of Expected_vs_Output_Difference_Test_Result dircmp object
        for Expected_vs_Output_Different_file in Expected_vs_Output_Difference_Test_Result.right_list:

            #Failure: Special Case when output folder (e.g., right_list) does not contain file within expected output folder (e.g., left_list)
            if Expected_vs_Output_Different_file not in Expected_vs_Output_Difference_Test_Result.left_list:
                #If counter_missing < 1, then within first loop of differences in expected output and output
                if (counter_missing < 1):
                    #Announce there will be differences due to output folder not containing expected output
                    #Add tab indent
                    ListsOfTests_Results_file.write("\t")
                    #Write test case failed due to missing file outputs, and list missing file
                    ListsOfTests_Results_file.write("Test case failed due to the following expected output file missing from the new output: \n")
                    
                #Announce each difference
                #Add double tab indent
                ListsOfTests_Results_file.write("\t\t")
                #Write Expected_vs_Output_Different_file file, which is file that is not present
                ListsOfTests_Results_file.write(Expected_vs_Output_Different_file)
                #Add line return
                ListsOfTests_Results_file.write("\n")

                #Write to string the Folder name and test result = Failed
                ListsOfTests_Results_string["Overall Test Results"]="Failed"
                ListsOfTests_Results_string[ListOfTests_Input_Row[0]] = "Failed"
                
                #advance counter_missing
                counter_missing = counter_missing + 1

        #Failure: All Cases when Expected_vs_Output_Difference_Test_Result contains files that are different
        if len(Expected_vs_Output_Difference_Test_Result.diff_files) != 0:
            #Add tab indent
            ListsOfTests_Results_file.write("\t")
            #Write why test case failed
            ListsOfTests_Results_file.write("Test case failed due to following output files containing different content from expected output:\n")

            #Write to string the Folder name and test result = Failed
            ListsOfTests_Results_string["Overall Test Results"]= "Failed"
            #write specific test case result as failed
            ListsOfTests_Results_string[ListOfTests_Input_Row[0]] =  "Failed"

            #Loop through all failed tests between output and expected output folders 
            for diff in Expected_vs_Output_Difference_Test_Result.diff_files:
                #Add double tab indent
                ListsOfTests_Results_file.write("\t\t")
                #Write diff file to ListsOfTests_Results_file
                ListsOfTests_Results_file.write(diff+ " ")
                #Add line return
                ListsOfTests_Results_file.write("\n")
                 
        ListsOfTests_Results_file.write("==============================================================================\n\n")
        #close ListsOfTests_Results_file_name
        ListsOfTests_Results_file.close()
		 
#print to screen out ListsOfTests_Results_string
print('\n\nSummary of Tests: {}.'.format(ListsOfTests_Results_string))
print('\nReview file ..\\{} for details.'.format(ListsOfTests_Results_file_name))

#TimeDate_StopProcessingSimulation_seconds (s) is clock time when simulation processing ends
TimeDate_StopProcessingSimulation_seconds = time.time()
#Time_DurationToProcessSimulation_seconds (s) is difference between start and stop of processing simulation
Time_DurationToProcessSimulation_seconds = TimeDate_StopProcessingSimulation_seconds - TimeDate_StartProcessingSimulation_seconds
#Print out simulation runtime
print("\n" + "TestCases runtime was: {:.1f} minutes".format(Time_DurationToProcessSimulation_seconds/60))

#Write out the exe modified time
DateModified = TimeDate_StartProcessingSimulation_seconds - os.path.getmtime(HydroPlusExe_path)
print ("WeatherPrep.exe file used in test cases has vintage: {:.1f} minutes, or {:.1f} days.".format(DateModified/60, DateModified/60/60/24))

#open ListsOfTests_Results_file_name TestResults.txt as ListsOfTests_Results_file
with open(ListsOfTests_Results_file_name,'a') as ListsOfTests_Results_file:
    #for each ListOfTests_Input_Row result in ListsOfTests_Results_string
    for TestResults_file in ListsOfTests_Results_string:

        #write result of ListOfTests_Input_Row
        ListsOfTests_Results_file.write(TestResults_file + ":\t" + str(ListsOfTests_Results_string[TestResults_file]))
        ListsOfTests_Results_file.write("\n")

ListsOfTests_Results_file.close()

