#!/usr/bin/python
# -*- Mode: python -*-

# Copyright (C) 2001-2020 Artifex Software, Inc.
# All Rights Reserved.
#
# This software is provided AS-IS with no warranty, either express or
# implied.
#
# This software is distributed under license and may not be copied,
# modified or distributed except as expressly authorized under the terms
# of the license contained in the file LICENSE in this distribution.
#
# Refer to licensing information at http://www.artifex.com or contact
# Artifex Software, Inc.,  1305 Grant Avenue - Suite 200, Novato,
# CA 94945, U.S.A., +1(415)492-9861, for further information.
#


# overview
# 	update revision (default is HEAD)
# 	run all testfiles with all parameter sets using the gs executable - calculate each checksum
# 	compare checksums with 1) previous results 2) baseline checksums
# 	report baseline changes
# 	email results
# 
# temporary output:
#	the output images from gs execution are not saved
#
# permanent output:
# 	checksum database for the day (an "anydbm" database [testfile-device,checksum]
# 
# input:
# 	testing.cfg - configuration many significant variables (in /home/regression/regression, or .)
# 	command line - control details of execution
# 	files in compare file directory (ususally /home/regression/comparefiles)
# 	The following are "anydbm" databases as described above
#	 	database of checksums of the raster files in /home/regression/raster
#	 	databases of past nightly runs in /home/regression/regression/daily (only the most recent is used)
# 	baseline.log - log of updates to baseline raster files (in /home/regression/regression/baseline) [text file]
# 	
# Note that the raster files themselves are not used in this process.
# 
# Hierarchy of python scripts
# 	run_nightly.py
#		gsconf.py - read testing.cfg and create an object defining the environment for all other scripts
# 		run_regression.py - execute gs, calculate checksum, insert into database
#			gs_checkall.py
#				gs_checkraster.py
#				gs_checkpdfwrite.py
#					gsparamset.py - define the "parameter set" - the set of output devices to be tested for each file
#					gstestgs.py, gstestutil.py - run gs (used the built-in Python unit test harness)
#					gssum.py - checksum calculation and database addition
#			
# 		testdiff.py - compare three checksum databases
# 		get_baseline_log.py - extract recent changes to the baseline.log 
#
#
#
#



import os
import sys, shutil
import re
import time
import optparse, myoptparse
import string
import build_revision
import gsconf, gsparamsets
import anydbm
from popen2 import Popen4

# configuration variables

myself="unknown"
myversion="2.01"

def logMessage(message,file,revision,printMessage=True):
    message_time=time.strftime("%Y-%m-%d-%H:%M:%S", time.localtime())
    message=myself+" "+revision+" "+message_time+" "+message
    if printMessage:
        print message
    message+="\n"
    if file:
        file.write(message)
        file.flush()

def die(msg):
    sendmail(gsconf.report_from, gsconf.report_to, "error running regression", msg)
    sys.exit(0)

def read_all_lines(f):
    file = open(f, 'r')
    lines = file.readlines()
    file.close()
    return string.join(lines, '')

def get_count_in_db(dbname):
    try:
        db = anydbm.open(dbname, 'r')
        if not db:
            count=0
        else:
            count=len(db)
    except:
        count=0
    print dbname,count

def get_revision(dir=None):
    if dir:
        cwd=os.getcwd()
        os.chdir(dir)

    command="svn update --non-recursive"
    if os.system(command) != 0:
        message = "run_nightly.py:get_revision.py: cannot svn update"
        logMessage(message,None,"unknown")
        return "unknown"

    command="svn info"
    p = os.popen(command)
    for line in p:
        if "Revision:" in line:
            revision=line.strip('Revision: ')
            revision=revision.strip('\n')
	    break
    else:
        revision = None
    if dir:
        os.chdir(cwd)
    return revision

def change_gsproduct(file):
    tmpfile = "%s.tmp" % (file,)

    startre = re.compile("^#ifndef\ GS_PRODUCT$")
    changere = re.compile("^.*?\"[A-Za-z -]+\".*?$")
    endre = re.compile("^$")

    old = open(file, "r")
    new = open(tmpfile, "w")

    state = 0
    for line in old.readlines():
        print "checking:", line
        if state == 0:
            m = startre.search(line)
            if m:
                state = 1
                
            new.write(line)
        elif state == 1:
            m = changere.search(line)
            if m:
                state = 2
                new.write("\t\"GPL Ghostscript\"\n")
		print "rewriting '%s' to GPL Ghostscript" % line.strip()
            else:
                new.write(line)
        elif state == 2:
            m = endre.search(line)
            if m:
                state = 0

            new.write(line)

    old.close()
    new.close()

    os.unlink(file)
    os.rename(tmpfile, file)

def sendmail(frm, to, subject, text):
    import smtplib

    keyword = gsconf.mailkeyword
    if keyword:
        msg = 'From: %s\r\nTo: %s\r\nSubject: %s\r\nX-Fnord: %s\r\n\r\n%s' % (frm, to, subject, keyword, text)
    else:
        msg = 'From: %s\r\nTo: %s\r\nSubject: %s\r\n\r\n%s' % (frm, to, subject, text)

#   server = smtplib.SMTP(gsconf.mail_server)
#   server.sendmail(frm, to, msg)
#   server.quit()
    SENDMAIL = "/usr/sbin/sendmail" # sendmail location
    p = os.popen("%s -t" % SENDMAIL, "w")
    p.write(msg)
    p.close()

def run_nightly(options,arguments):
    global myself

    myself=options.myself
    # what do we work on?
    revision      = options.revision
    release       = options.release
    
    # what do we build
    update           = not options.noupdate
    
    options.svn       = not options.nosvn
    options.configure = not options.noconfigure
    options.make      = not options.nomake
    options.makeclean = not options.nomakeclean
    
    options.remove   = False  # never remove the gs executable, we need it for regression testing
    
    # what do we run?
    runRegression    = not options.noregression
    testRegression   = not options.notestregression
    baselineChanges  = not options.nobaselinechanges
    printRegression  = options.printregression
    
    # where do stdout and stderr go?
    options.capture  = not options.nocapture
    
    p = os.popen('hostname -s')
    if p:
        hostname = string.strip(p.readline())
        p.close()
    else:
        hostname = 'unknown host'
        
    p = os.popen('uname -m -s')
    if p:
        hostuname=string.strip(p.readline())
        p.close()
    else:
        hostuname=""

    hostname=hostname + " (" + hostuname + ')'

    if release:
        print "NOT SUPPORTED: need to build a release",release
        sys.exit(1)

    if not revision or revision == "HEAD":
        gsroot=gsconf.gsroot
        revision_value=get_revision(gsroot)
        gsinstall=gsconf.installtree
        gspath=gsconf.installtree+"bin/gs"
        revision_full="HEAD:"+revision_value
    else:
        gsroot=gsconf.root+"gs."+revision+"/"
        gsconf.checksumdb = revision
        revision_value=revision
        gsrevision_root=gsconf.root+"gs."+revision+"/"
        gspath=gsrevision_root+"bin/gs"
        gsinstall=None
        revision_full=revision_value

    logdir=gsconf.logdir
    if not os.path.exists(logdir):
        os.mkdir(logdir)

    now=time.strftime("%Y-%m-%d-%H:%M:%S", time.localtime())
    mail_message = now+" "+revision_value+"\n"

    prefix=logdir+revision_value+"."

    update_stdout=prefix+gsconf.update_stdout
    update_stderr=prefix+gsconf.update_stderr
    make_stdout=prefix+gsconf.make_stdout
    make_stderr=prefix+gsconf.make_stderr
    config_stdout=prefix+gsconf.config_stdout
    config_stderr=prefix+gsconf.config_stderr
    install_stdout=prefix+gsconf.install_stdout
    install_stderr=prefix+gsconf.install_stderr
    
    cumulative_name=prefix+gsconf.cumulative
    
    history_name=logdir+gsconf.history        # permanent file, accumulates history across days
    
    if not os.path.exists(history_name):
        messageHistory="creating history file "+history_name
    else:
        messageHistory="opening history file "+history_name
    history_file = open(history_name, "w")    

    cumulative_file = open(cumulative_name, "w")
    message="cumulative"
    logMessage(message,cumulative_file,revision_full,printMessage=False)
    logMessage(messageHistory,cumulative_file,revision_full,printMessage=False)

    message="running on " + hostname +" my version "+myversion
    logMessage(message,cumulative_file,revision_full)
    logMessage(message,history_file,revision_full,printMessage=False)
    if options.version:
        sys.exit(1)

    if options.removeonly:
        path=gsroot
        message="removing "+path
        logMessage(message,cumulative_file,revision_full)
        shutil.rmtree(path,ignore_errors=True)
        if gsinstall:
            path=gsinstall
            message="removing "+path
            logMessage(message,cumulative_file,revision_full)
            shutil.rmtree(path,ignore_errors=True)
        sys.exit(1)

    mail_message+=message+"\n"

    if not os.path.exists(gsconf.comparefiledir + "/.svn") or \
	    os.system("svn update " + gsconf.comparefiledir) != 0:
	message = "run_nightly.py: cannot svn update " + gsconf.comparefiledir
	logMessage(message,None,"n/a")

    if not os.path.exists(gsconf.comparefiledir + "/../tests/eps" + "/.svn") or \
	    os.system("svn update " + gsconf.comparefiledir + "/../tests/eps") != 0:
	message = "run_nightly.py: cannot svn update " + gsconf.comparefiledir + "/../tests/eps"
	logMessage(message,None,"n/a")
    if os.path.exists(gsconf.comparefiledir + "/../tests/eps"):
        os.system("ln -f -s " + gsconf.comparefiledir + "/../tests/eps/* " + gsconf.comparefiledir)
    if not os.path.exists(gsconf.comparefiledir + "/../tests/ps" + "/.svn") or \
	    os.system("svn update " + gsconf.comparefiledir + "/../tests/ps") != 0:
	message = "run_nightly.py: cannot svn update " + gsconf.comparefiledir + "/../tests/ps"
	logMessage(message,None,"n/a")
    if os.path.exists(gsconf.comparefiledir + "/../tests/ps"):
        os.system("ln -f -s " + gsconf.comparefiledir + "/../tests/ps/* " + gsconf.comparefiledir)
    if not os.path.exists(gsconf.comparefiledir + "/../tests/pdf" + "/.svn") or \
	    os.system("svn update " + gsconf.comparefiledir + "/../tests/pdf") != 0:
	message = "run_nightly.py: cannot svn update " + gsconf.comparefiledir + "/../tests/pdf"
	logMessage(message,None,"n/a")
    if os.path.exists(gsconf.comparefiledir + "/../tests/pdf"):
        os.system("ln -f -s " + gsconf.comparefiledir + "/../tests/pdf/* " + gsconf.comparefiledir)
    comparefiles = os.listdir(gsconf.comparefiledir)
    count=len(comparefiles)

    message="comparefiles from "+gsconf.comparefiledir+" number of files "+str(count)
    logMessage(message,cumulative_file,revision_full)
    logMessage(message,history_file,revision_full,printMessage=False)
    mail_message+=message+"\n"

    # use the smallest that is requested
    if options.oneset:    
        gsparamsets.testparamsets = gsparamsets.testparamsets_one
        gsparamsets.pdftestparamsets = gsparamsets.pdftestparamsets_one
        gsparamsets.testparamsets_name = "one"
    elif options.minset:
        gsparamsets.testparamsets = gsparamsets.testparamsets_minimum
        gsparamsets.pdftestparamsets = gsparamsets.pdftestparamsets_minimum
        gsparamsets.testparamsets_name = "minimum"
    elif options.maxset:    
        gsparamsets.testparamsets = gsparamsets.testparamsets_maximum
        gsparamsets.pdftestparamsets = gsparamsets.pdftestparamsets_full
        gsparamsets.testparamsets_name = "maximum"
    elif options.fullset:    
        gsparamsets.testparamsets = gsparamsets.testparamsets_full
        gsparamsets.pdftestparamsets = gsparamsets.pdftestparamsets_full
        gsparamsets.testparamsets_name = "full"
    else:
        pass # use default in gsparamsets

    gsparamsets.testparamsets = gsparamsets.testparamsets_one
    gsparamsets.pdftestparamsets = gsparamsets.pdftestparamsets_full
    gsparamsets.testparamsets_name = "one+pdf"


    message="test parameter set name: "+gsparamsets.testparamsets_name
    logMessage(message,cumulative_file,revision_full)
    logMessage(message,history_file,revision_full,printMessage=False)
    mail_message+=message+"\n"

    message="logging to "+prefix+"*"
    logMessage(message,cumulative_file,revision_full)
    mail_message+=message+"\n"

    message="sending mail to "+gsconf.report_to
    logMessage(message,cumulative_file,revision_full)
    logMessage(message,history_file,revision_full,printMessage=False)

    if update:
        message="starting update_ghostscript"
        logMessage(message,cumulative_file,revision_full)
    
        (err,message) = build_revision.update_ghostscript(revision,release,
                                                          gsroot,
                                                          options,
                                                          update_stdout,update_stderr,
                                                          config_stdout,config_stderr,
                                                          make_stdout,make_stderr,
                                                          install_stdout,install_stderr,
                                                          cumulative_file
                                                          )
        if err != 0:
            die(message)
    else:
        message="skipping update of ghostscript tree and executable"
        logMessage(message,cumulative_file,revision_full)

    message = "running makemissing"
    logMessage(message,cumulative_file,revision_full)
    os.system("./makemissing 2>logs/makemissing.stderr >logs/makemissing.stdout")


    prefix=logdir+revision_value+"."

    baselines_name=prefix+gsconf.baselines
    differences_name=prefix+gsconf.differences
    regression_name=prefix+gsconf.regression
    email_name=prefix+gsconf.email

    if not os.path.exists(gspath):
        message=myself+" FATAL "+"the gs executable does not exist "+gspath
        logMessage(message,cumulative_file,revision_full)
        die(message)

    regression_file = open(regression_name, "w")
    message="regression"
    logMessage(message,regression_file,revision_full,printMessage=False)

    message="revision of executable "+revision_full
    logMessage(message,regression_file,revision_full,printMessage=False)
    logMessage(message,history_file,revision_full,printMessage=False)
    mail_message+=message+"\n"

    revision_name=prefix+revision
    revision_file = open(revision_name, "w")
    revision_file.close()

    # create string "regression_results" for regression results
    regression_results = '\n'
    if runRegression:
        print

        message="starting regression testing on "+revision
        logMessage(message,regression_file,revision_full,printMessage=False)
        logMessage(message,cumulative_file,revision_full)

        timearg=" --time="+"\""+now+"\""
        trackarg=" --track"

        # revision can be HEAD
        if revision != "HEAD":
            revisionarg=" --revision="+revision
        else: revisionarg=""
   
        # revision_value always has the revision number, also for HEAD
        revision_valuearg=" --revision_value="+revision_value

        if release:
            releasearg=" --release="+release
        else: releasearg=""
    
        command=gsconf.run_regression_script + releasearg + revisionarg + revision_valuearg + timearg + trackarg

        if options.capture:
            capture=" 2>&1"
            command+=capture

        message=command
        logMessage(message,regression_file,revision_full,printMessage=False)
        logMessage(message,cumulative_file,revision_full)

        regression_results += '\nThe complete list of regressions for today:\n'
        regression_results += command +"\n"

        p = os.popen(command)
        if p == None:
            message="cannot open and run the regression script."
            logMessage(message,cumulative_file,revision_full)
            die(message)

        pattern = 'ok$'
        for line in p.readlines():
            if printRegression: print line
            if not re.search(pattern, line):
                regression_results += line
        p.close()

        # count number of entries in daily db
        if gsconf.__dict__.has_key("checksumdb") and gsconf.checksumdb:
            newchecksum=gsconf.checksumdb
        else:
            newchecksum=time.strftime("%Y%m%d", time.localtime())

    else:
        message="skipping regression testing"
        logMessage(message,regression_file,revision_full,printMessage=False)
        logMessage(message,cumulative_file,revision_full)

        regression_results+="gs regression processing was skipped"
    # end if runRegression

    regression_results += '\n'

    # create string "difference_results" for regression results
    if testRegression:
        print

        if gsconf.__dict__.has_key("checksumdb") and gsconf.checksumdb:
            newchecksum=gsconf.checksumdb
        else:
            newchecksum=time.strftime("%Y%m%d", time.localtime())

#        message="verify existence of the checksum database "+newchecksum
#        logMessage(message,cumulative_file,revision_full,printMessage=False)
#        logMessage(message,regression_file,revision_full)

        message="starting difference_results"
        logMessage(message,cumulative_file,revision_full,printMessage=False)
        logMessage(message,regression_file,revision_full)

        if options.oldrevision:
            oldchecksum=options.oldrevision
        else:
            # find the most recent checksum file in the gsconf.dailydir
            oneday=24*60*60
            testtime=time.time()
            for count in range(1,30):
                testtime-=oneday
                oldchecksum=time.strftime("%Y%m%d", time.localtime(testtime))
                datafile=gsconf.dailydir+oldchecksum+".db"
                if os.path.exists(datafile):
                    try:
                        db = anydbm.open(datafile)
                        if db:
                            db_hasdata=True
                            db.close()
                            if db_hasdata:
                                message="old checksum database used "+datafile
                                logMessage(message,cumulative_file,revision_full,printMessage=True)
                                break
                            else:
                                message="old checksum database empty "+datafile
                                logMessage(message,cumulative_file,revision_full,printMessage=True)
                    except:
                        message="old checksum database could not be opened "+datafile
                        logMessage(message,cumulative_file,revision_full,printMessage=True)
                else:
                    message="old checksum database does not exist "+datafile
                    logMessage(message,cumulative_file,revision_full,printMessage=True)
            else:
                message="cannot find any recent checksum database file",
                logMessage(message,cumulative_file,revision_full)
                message="FATAL cannot find any recent checksum database file"
                logMessage(message,cumulative_file,revision_full)
                die(message)

        oldchecksumpath = gsconf.dailydir + oldchecksum + ".db"
        if not os.path.exists(oldchecksumpath):
            message=myself+" FATAL "+"the old checksum database does not exist "+oldchecksumpath
            logMessage(message,cumulative_file,revision_full)
            die(message)

        recent_ctime = time.ctime(testtime)

        message="found recent checksum database "+oldchecksum
        logMessage(message,cumulative_file,revision_full,printMessage=False)
        logMessage(message,regression_file,revision_full)
        logMessage(message,history_file,revision_full,printMessage=False)

        difference_results = 'The following regression changes happened since recent report: '+oldchecksum
        difference_results += '\n'

        command=gsconf.testdiff_script + " " + oldchecksum + " " + newchecksum
        if options.capture:
            capture=" 2>&1"
            command+=capture

        logMessage(command,regression_file,revision_full,printMessage=False)
        logMessage(command,cumulative_file,revision_full)
        p = os.popen(command)
        if p:
            for line in p.readlines():
                if printRegression: print line
                difference_results += line
            p.close()
        else:
            difference_results = difference_results + 'no results from difference script\n'

        message="get baseline updates"
        logMessage(message,regression_file,revision_full,printMessage=False)
        logMessage(message,cumulative_file,revision_full)


        message="marcos: about to call get_baseline_log_script"
        logMessage(message,cumulative_file,revision_full)

        # Get updated baseline_changes
        baseline_changes = 'The following files had their baselines updated:\n'

        command=gsconf.get_baseline_log_script + " '" + recent_ctime + "'"
        baseline_changes += command+"\n"

        logMessage(command,cumulative_file,revision_full)
        p = Popen4(command,1)
        if p:
            for line in p.fromchild.readlines():
                baseline_changes += line
                p.wait()
        else:
            baseline_changes = baseline_changes + 'no results from baseline script\n'
            baseline_changes += "\n"

        message="marcos: done with get_baseline_log_script"
        logMessage(message,cumulative_file,revision_full)

        logMessage(baseline_changes,regression_file,revision_full)
        logMessage(difference_results,regression_file,revision_full)
    else:
        difference_results="difference testing was skipped"
    difference_results += '\n'
    # end if testRegression
    
    message="marcos: writing to differences file"
    logMessage(message,cumulative_file,revision_full)

    differences_file = open(differences_name, "w")
    message="differences"
    differences_file.write(message)
    logMessage(message,differences_file,revision_full,printMessage=False)


    if baselineChanges:
        print

        baselines_file = open(baselines_name, "w")
        message="baseline changes"
        baselines_file.write(message)
        logMessage(baseline_changes,baselines_file,revision_full,printMessage=False)

    else:
        baseline_changes = "difference testing was skipped"
        
    message="marcos: done writing to differences file"
    logMessage(message,cumulative_file,revision_full)

    # end if baselineChanges

    mail_message+=difference_results+"\n"
    mail_message+=regression_results+"\n"
    mail_message+=baseline_changes+"\n"

    email_file = open(email_name, "w")
    subject='gs regression report - %s - revision: %s' % (now,revision_full)

    message="subject "+subject
    logMessage(message,cumulative_file,revision_full)
    logMessage(message,email_file,revision_full,printMessage=False)

    message="send mail to "+gsconf.report_to
    logMessage(message,cumulative_file,revision_full)
    logMessage(message,email_file,revision_full,printMessage=False)

    logMessage(mail_message,email_file,revision_full,printMessage=False)
    email_file.close()

    if options.printmailmessage:
        print
        print "mail_message start"
        print mail_message
        print "mail_message end"    
        print

    while True:
        try:
            print "sendmail"
            sendmail(gsconf.report_from, gsconf.report_to, subject, mail_message)
            break
            time.sleep(5)
        except:
            print "sendmail fail"
            time.sleep(15)
            pass

    message="done"
    logMessage(message,cumulative_file,revision_full)

    regression_file.close()
    cumulative_file.close()
    history_file.close()

    print regression_name
    print cumulative_name
    print regression_name

    return 0

#########################################################

if __name__ == "__main__":

    os.umask(0002)

    optionsParser=optparse.OptionParser()

    optionsParser.add_option('--version',action='store_true',help="get my version")

    optionsParser.add_option('--removeonly',action='store_true',help="remove the build directory - do nothing else")

    optionsParser.add_option('--noupdate',action='store_true',help="do not update the gs executable")
    # "update" includes svn, configure, makeclean, clean, install

    optionsParser.add_option('--nosvn',action='store_true',help="do not update the source from the svn repository")
    optionsParser.add_option('--noconfigure',action='store_true',help="do not run auto configure")
    optionsParser.add_option('--nomakeclean',action='store_true',help="do not make clean before make")
    optionsParser.add_option('--nomake',action='store_true',help="do not make")

    optionsParser.add_option('--nocapture',action='store_true',help="do not capture stdout and stderr from commands")


    optionsParser.add_option('--noregression',action='store_true',help="do not run regression")
    optionsParser.add_option('--notestregression',action='store_true',help="do not test regression")
    optionsParser.add_option('--nobaselinechanges',action='store_true',help="do not analyze baselinechanges")
    optionsParser.add_option('--printregression',action='store_true',help="print regression output")
    optionsParser.add_option('--printmailmessage',action='store_true',help="print mail message")

    optionsParser.add_option('--fullset',action='store_true',help="full gsparamsets")
    optionsParser.add_option('--minset',action='store_true',help="minimum gsparamsets")
    optionsParser.add_option('--maxset',action='store_true',help="maximum gsparamsets")
    optionsParser.add_option('--oneset',action='store_true',help="one gsparamsets")    

    optionsParser.add_option('--release',action='store_true',help="under contruction")
    optionsParser.add_option('--oldrevision',action='store',help="old revision for checksum comparison",default=None)

    (options,arguments)=myoptparse.parseCommandLine(optionsParser,deviceSkip=True,testfileSkip=True,listfileSkip=True)

    if options.version:
        print options.myself,"version",myversion
        sys.exit(1)

    result = run_nightly(options,arguments)

    sys.exit(result)
