classdef TestExtender  < handle
%

%   Copyright 2016-2022 The MathWorks, Inc.

    properties(Access = 'public')
        sutH;
        mdlH;
        context;
        datafolder;
        extopts;      
    end
    
    properties(Access = 'private')
        testdataMap;
        covdataMap;
    end
    
    methods(Access = 'private')
        
        
        function covdata = simsldvdata(obj, testdata)
            % currently it is set only for design model level run
            covdata = [];
            if isequal(obj.sutH, obj.mdlH)
                sldvdata = testdata.sldvdata;
                if ~isempty(sldvdata)
                    cvt = cvtest(obj.mdlH);
                    cvt.modelRefSettings.enable          = 'On';
                    cvt.modelRefSettings.excludeTopModel = 0;
                    cvt.modelRefSettings.excludedModels  = '';
                    cvt.emlSettings.enableExternal       = 1;

                    runOpts = sldvruntestopts;
                    runOpts.coverageEnabled = true;
                    runOpts.coverageSetting = cvt;

                  [~, covdata] = ...
                      sldvruntest(obj.mdlH, sldvdata, runOpts);
                end
            end
        end
        
        function covdata = simsldvharness(obj, testdata)
            covdata    = [];
            options    = sldvoptions(obj.mdlH);
            % Need to explicitly enable TestGeneration. The deepCopy is
            % needed to prevent dirtying of the model.
            options = options.deepCopy;
            options.Mode = 'TestGeneration';
            harnessMdl = testdata.data;
            harnessH   = get_param(harnessMdl, 'handle');
            harnessSource = Sldv.harnesssource.Source.getSource(harnessH);
            time  = harnessSource.getTimeAsCellArray;
            
            if strcmpi('inf', get_param(harnessH,'StopTime'))
                maxSimulationTime = 0;
                for time_set = time(:)'
                    maxSimulationTime = max(maxSimulationTime, max(time_set{:}));
                end
                paramStruct.StopTime = sldvshareprivate('util_double2str', maxSimulationTime);
            end
            cvtobj              = cvtest(harnessH);
            cvtobj.modelRefSettings.enable          = 'On';
            cvtobj.modelRefSettings.excludeTopModel = 0;
            cvtobj.modelRefSettings.excludedModels  = '';
            cvtobj.emlSettings.enableExternal       = 1;
            
            if strcmpi(options.IncludeRelationalBoundary, 'off')
                cvtobj.settings.relationalop = 0;
            else
                cvtobj.settings.relationalop = 1;
            end
            
            cvtobj.settings.mcdc = 1;
            cvtobj.settings.condition = 1;
            cvtobj.settings.decision = 1;
            if strcmpi(options.getDerivedModelCoverageObjectives(), 'conditiondecision')
                cvtobj.settings.mcdc = 0;
            else
                if strcmpi(options.getDerivedModelCoverageObjectives(), 'decision')
                    cvtobj.settings.mcdc = 0;
                    cvtobj.settings.condition = 0;
                else 
                    if strcmpi(options.getDerivedModelCoverageObjectives(), 'none')
                        cvtobj.settings.mcdc = 0;
                        cvtobj.settings.condition = 0;
                        cvtobj.settings.decision = 0;
                    end
                end
            end
            
            if strcmpi(options.TestObjectives, 'disableall')
                cvtobj.settings.designverifier = 0;
            else
                if strcmpi(options.TestObjectives, 'enableall')
                    cvtobj.settings.designverifier = 1;
                end
                %check the case when use local settings is selected
            end
                        
            num_of_test_cases = harnessSource.getNumberOfTestcases; 
            for i=1:num_of_test_cases
                harnessSource.setActiveTestcase(i);
                if strcmpi('inf', get_param(harnessH,'StopTime'))
                    cvd = cvsim(cvtobj, paramStruct);
                else
                    cvd = cvsim(cvtobj);
                end
                if i==1
                    covdata = cvd;
                else
                    covdata = covdata + cvd;
                end
            end
            
        end
        
        function covdata = simtestsuite(obj, testdata)
            covdata = [];
            ttype = testdata.type;
            switch ttype
                case 'SLDVDATA'
                    covdata = simsldvdata(obj, testdata);
                case 'SLDVHARNESS'
                    covdata = simsldvharness(obj, testdata);
            end    
        end
        
        function setupFolderToStoreIntermediateData(obj)
          
            options = sldvoptions(obj.mdlH);
            % read default output directory in which results will be produced
            % check if $ModelName$ is present in outdir, if so replace it with model
            % name
            mdlName = get_param(obj.mdlH, 'Name');
            dirName = strrep(options.OutputDir, '$ModelName$', mdlName);
            dirName = strrep(dirName, '\', filesep);
            dirName = strrep(dirName, '/', filesep);
            dirName = fullfile(pwd, dirName);
            if( ~exist(dirName, 'dir'))
                mkdir(dirName);
            end
            obj.datafolder = dirName;
        end
        
        function fullpath = generateFileName(obj, fname, extn)
             fullpath = Sldv.utils.uniqueFileNameUsingNumbers(obj.datafolder,fname, extn);
        end
        
        function isValidExtnopts = checkIfValidExtnopts(~, extnOpts)
            fd = fieldnames(extnOpts);
            for i = 1: numel(fd)
                if ~isequal(fd{i}, 'DVMaxProcessTime') && ~isequal(fd{i}, 'DVTestSuiteOptimization') ... 
                        && ~isequal(fd{i}, 'DVModelCoverageObjectives') && ~isequal(fd{i}, 'DVIncludeRelationalBoundary') ...
                        && ~isequal(fd{i}, 'DVTestObjectives') ...
                        && ~isequal(fd{i}, 'DVTestgenTarget')
                    isValidExtnopts = false;
                    return;
                end
            end
            isValidExtnopts = true;
        end
        
        function opts = copyExtnOptsInSldvOpts(obj, opts, extnopts)
            if isempty(extnopts)
                return;
            else
                if ~checkIfValidExtnopts(obj, extnopts)
                    error('Sldv:TopItOff:InvalidExtnOpts',getString(message('Sldv:TopItOff:InvalidExtnOpts'))); 
                end
            end
            
            if isfield(extnopts,'DVMaxProcessTime')
                opts.MaxProcessTime        = extnopts.DVMaxProcessTime;
            end
            if isfield(extnopts,'DVTestSuiteOptimization')
                opts.TestSuiteOptimization = extnopts.DVTestSuiteOptimization;
            end
            if isfield(extnopts,'DVTestgenTarget')
                opts.TestgenTarget = extnopts.DVTestgenTarget;
            end
        end
        
        function opts = getCovExtnSLDVOpts(obj, totalcov, extnopts)
            opts = sldvoptions(obj.mdlH);
            opts = opts.deepCopy();
           
            % first save both files
            if ~isempty(totalcov)
                opts.IgnoreCovSatisfied  = 'on';
                fullpath = generateFileName(obj,'TestExtenderCovData','.cvt');
                cvsave(fullpath, totalcov);
                opts.CoverageDataFile = fullpath;
            end
            
            opts = copyExtnOptsInSldvOpts(obj, opts, extnopts);
            opts.SaveHarnessModel = 'off'; %g1384917 -- As we are doing missing coverage on harness model, 
            %we do not generate another harness. If this is not done, it throws error in 
            %sldvgencov() and in sldvmakeharness()
            
            %g1407966: Change mode to TestGeneration. (Mode might be design
            %error detection or property proving in the referenced model)
            opts.Mode = 'TestGeneration';
        end
        
        function opts = getTestExtnSLDVOpts(obj, totalcov, sldvData, extnopts)
            % Check the validity of sldvData structure
            assert(isempty(sldvData) || isfield(sldvData, 'TestCases'), ...
                'sldvData structure format is invalid');

            opts = obj.getCovExtnSLDVOpts(totalcov, extnopts);
            
            if ~isempty(sldvData)
                fullpath = generateFileName(obj,'TestExtendersldvData','.mat');
                save(fullpath, 'sldvData');
                opts.ExistingTestFile = fullpath;
                opts.ExtendExistingTests = 'on';
            end
        end
        
        function [intype, sldvdata] = convertToSldvData(~, tdata)
            if isa(tdata,'Simulink.SimulationData.Dataset')
                intype   = 'DATASET';
                sldvdata = struct([]);
            else
                intype   = 'SLDVDATA';
                if ischar(tdata)
                    % we have path of sldvdata
                    d        = load(tdata);
                    sldvdata = d.sldvData;
                else
                    sldvdata = tdata;
                end
            end
        end
        
    
        function totalcov = collectTotalCoverage(obj)
            % check if the there is already coverage data available for
            % refH
            totalcov = [];
            covdata  = [];
            covpresent  = isKey(obj.covdataMap, obj.sutH);
            if covpresent
                covdata  = obj.covdataMap(obj.sutH);
                totalcov = covdata.cumulative;
            end
            % for a given refH, collect coverage by simulating all test data
            % for refH.
            
            % To avoid, repeated simulation of test data to get coverage,
            % we update the coverage data in  coverage map once any test data is simulated.
            % Thus, we store the last index of the updated test data in
            % coverage map for the model.
            testpresent = isKey(obj.testdataMap, obj.sutH);
            if testpresent
                testdataArr = obj.testdataMap(obj.sutH);
                if covpresent
                    lastindex = covdata.cachedTestIndex + 1;
                else 
                    lastindex = 1;
                end
                updated = false;
                for i=lastindex:length(testdataArr)
                    cvdata  = simtestsuite(obj, testdataArr(i));
                    if isempty(totalcov)
                        totalcov = cvdata;
                    else
                        totalcov = totalcov + cvdata;
                    end
                    updated  = true;
                end
                if updated
                    % note that this may create a new entry for refH in coverage map     
                    covdata.cumulative       = totalcov;
                    covdata.cachedTestIndex  = length(testdataArr);
                    obj.covdataMap(obj.sutH) = covdata;
                end
            end        
            % check if user wants to include any coverage from hierarchy or
            % not. 
            % currently we are ignoring setting in 'extendopts'
        end
            
        
    end

    methods( Access = 'public')
        
        function obj = TestExtender(sutH)
            try 
                if ischar(sutH)
                    obj.sutH = get_param(sutH, 'handle');
                else
                    obj.sutH = sutH;
                end
            catch mex
                throw(mex);
            end
            obj.mdlH = bdroot(obj.sutH);
            % check if sutH is same as mdlH to get the extension context
            if isequal(obj.mdlH, obj.sutH)
                obj.context = 'EXTEND_SYSTEM_CONTEXT';
            else
                obj.context = 'EXTEND_UNIT_CONTEXT';
            end
            
            % generate empty maps to store coverage and test data model
            % element wise
            obj.testdataMap = containers.Map('KeyType', 'double', 'ValueType', 'any');
            obj.covdataMap  = containers.Map('KeyType', 'double', 'ValueType', 'any');
            obj.defaultOptions();
            
            setupFolderToStoreIntermediateData(obj);
        end
        
        
        function refkey = addHarness(obj, harnessH, unitH)
            if nargin < 2 || nargin > 3  %nargin should be at least 2 as harnessH must be passed
                error('Sldv:TopItOff:EmptyData', getString(message('Sldv:TopItOff:EmptyData')));
            end
            if nargin == 2
                refH = obj.sutH;
            else
                refH = get_param(unitH, 'handle');
            end
            if ~isequal(refH, obj.sutH)
                 error('Sldv:TopItOff:HarnessDataNotforSUT', getString(message('Sldv:TopItOff:HarnessDataNotforSUT', get_param(obj.sutH,'Name'), get_param(refH,'Name'))));
            end
               
            % get test data present for given model element
            present = isKey(obj.testdataMap, refH);
            if present
                alltestdata = obj.testdataMap(refH);
            else
                alltestdata = struct([]);
            end
            numdata            = length(alltestdata);
            newid              = numdata + 1;
            testdata.index     = newid;
            
            if ~ Sldv.HarnessUtils.isSldvGenHarness(harnessH)
                if ischar(harnessH)
                    harnessHName = harnessH;
                else
                    harnessHName = get_param(harnessH, 'Name'); 
                end
                error('Sldv:TopItOff:NoSldvHarness', getString(message('Sldv:TopItOff:NoSldvHarness', harnessHName)));
            end

            obj.extopts.DVTestgenTarget = 'Model';
            if sldv.code.internal.isXilFeatureEnabled()
                % Extract information from the model reference block (at
                % this stage all the checks have been made to ensure that
                % only one model reference is there)
                block = find_system(get_param(harnessH, 'Handle'), 'SearchDepth', 1, 'BlockType', 'ModelReference');
                if strcmpi(get_param(block, 'SimulationMode'), SlCov.CovMode.toSimulationMode('SIL'))
                    codeIf = get_param(block, 'CodeInterface');
                    if strcmpi(codeIf, 'Model reference')
                        obj.extopts.DVTestgenTarget = Sldv.utils.Options.TestgenTargetGeneratedModelRefCodeStr;
                    else
                        obj.extopts.DVTestgenTarget = Sldv.utils.Options.TestgenTargetGeneratedCodeStr;
                    end
                end
            end

            testdata.type      = 'SLDVHARNESS';
            testdata.data      = harnessH;
            testdata.sldvdata  = struct([]);
            if newid == 1
                alltestdata = testdata;
            else 
                alltestdata(newid) = testdata;
            end
            
            % update testdatamap for new test suite available
            obj.testdataMap(refH) = alltestdata;
            
            % return refkey for future reference
            refkey.blk   = refH;
            refkey.index = newid;
        end
        
        
        
        function refkey = addTestData(obj, tdata, unitH)
            
            if nargin < 2 || nargin > 3
                error('Sldv:TopItOff:EmptyData', getString(message('Sldv:TopItOff:EmptyData')));
            end
            if nargin == 2
                refH = obj.sutH;
            else
                refH = get_param(unitH, 'handle');
            end
            if ~isequal(refH, obj.sutH)
                 error('Sldv:TopItOff:TestDataNotforSUT', getString(message('Sldv:TopItOff:TestDataNotforSUT', get_param(obj.sutH,'Name'), get_param(refH,'Name'))));
            end
               
            % get test data present for given model element
            present = isKey(obj.testdataMap, refH);
            if present
                alltestdata = obj.testdataMap(refH);
            else
                alltestdata = struct([]);
            end
            numdata            = length(alltestdata);
            newid              = numdata + 1;
            testdata.index     = newid;
            [intype, sldvdata] = obj.convertToSldvData(tdata);
            
            if isempty(sldvdata)
                error('Sldv:TopItOff:DataError', getString(message('Sldv:TopItOff:DataError')));
            end
            
            testdata.type      = intype;
            testdata.data      = tdata;
            testdata.sldvdata  = sldvdata;
            if newid == 1
                alltestdata = testdata;
            else 
                alltestdata(newid) = testdata;
            end
            
            % update testdatamap for new test suite available
            obj.testdataMap(refH) = alltestdata;
            
            % return refkey for future reference
            refkey.blk   = refH;
            refkey.index = newid;
        end
            
        function addCoverageData(obj, cvd, unitH)
            if nargin <= 1 || nargin > 3
                return;
            end
            if nargin == 2 
                refH = obj.mdlH;
            else
                refH = get_param(unitH, 'handle');
            end
            if ~isequal(refH, obj.sutH)
                 error('Sldv:TopItOff:CoverageDataNotforSUT', getString(message('Sldv:TopItOff:CoverageDataNotforSUT', get_param(obj.sutH,'Name'), get_param(refH,'Name'))));
            end
            if ischar(cvd)
                 [~, cvdata] = cvload(cvd);
                 cvdata = cvdata{1};
            else
                cvdata = cvd;
            end
            % get covdata present for given model element
            present = isKey(obj.covdataMap, refH);
            if present
               covdata = obj.covdataMap(refH);
               try
                   covdata.cumulative   = covdata.cumulative + cvdata;
                   obj.covdataMap(refH) = covdata;
               catch mex
                   throw(mex);
               end
            else
               covdata.cumulative = cvdata;
               % this captures coverage cached by executing test cases in
               % testdatamap
               covdata.cachedTestIndex = 0;
               obj.covdataMap(refH)    = covdata;
            end
        end
         
        function defaultOptions(obj)    
            opts  = sldvoptions(obj.mdlH);
            exopts.DVMaxProcessTime            = opts.MaxProcessTime;
            exopts.DVTestSuiteOptimization     = opts.TestSuiteOptimization;
            exopts.DVModelCoverageObjectives   = opts.ModelCoverageObjectives;
            exopts.DVIncludeRelationalBoundary = opts.IncludeRelationalBoundary;
            exopts.DVTestObjectives            = opts.TestObjectives;
            exopts.DVTestgenTarget             = 'Model';
            obj.extopts = exopts;
        end
        
        function opts = getOptions(obj)
            opts = obj.extopts;
        end
        
        function setOptions(obj, opts)
            % Check the sanity
            obj.extopts = opts;
        end
        
        function [extopts] = setExtnOpts(obj, extopts)
            if ~obj.checkIfValidExtnopts(extopts)
                error('Sldv:TopItOff:InvalidExtnOpts',getString(message('Sldv:TopItOff:InvalidExtnOpts')));
            else
                if ~isfield(extopts,'DVMaxProcessTime')
                    extopts.DVMaxProcessTime = obj.extopts.DVMaxProcessTime;
                end
                if ~isfield(extopts,'DVTestSuiteOptimization')
                    extopts.DVTestSuiteOptimization = obj.extopts.DVTestSuiteOptimization;
                end
                if ~isfield(extopts,'DVModelCoverageObjectives')
                    extopts.DVModelCoverageObjectives = obj.extopts.DVModelCoverageObjectives;
                end
                if ~isfield(extopts,'DVIncludeRelationalBoundary')
                    extopts.DVIncludeRelationalBoundary = obj.extopts.DVIncludeRelationalBoundary;
                end
                if ~isfield(extopts,'DVTestObjectives')
                    extopts.DVTestObjectives = obj.extopts.DVTestObjectives;
                end
                if ~isfield(extopts,'DVTestgenTarget')
                    extopts.DVTestgenTarget = obj.extopts.DVTestgenTarget;
                end
            end
        end
         
        function [status, newsldvdata, fullCovAlreadyAchieved, ...
                deltacov, filenames, refkey] = extendCoverage(obj, existingSldvData, extopts, generateDeltaCov, client)
             % update cumulative coverage by using configured test data
             % first and then use this to extend coverage
             % currently, we may have some gaps for subsystem related
             % workflows. e.g. how to get extracted model etc.
             
             if nargin > 5
                 error('Sldv:TopItOff:EmptyData', getString(message('Sldv:TopItOff:EmptyData')));
             end
             if nargin < 5
                 client = Sldv.SessionClient.DVCommandLine;
             end
             if nargin < 4
                 generateDeltaCov = false;
             end
             
             if nargin < 3 || isempty(extopts)
                     extopts = obj.extopts;
             else
                 extopts = obj.setExtnOpts(extopts);
             end
             if nargin < 2
                 existingSldvData = [];
             end

             % Pass generateDeltaCov as true if you want to generate deltaCoverage. Here sldvgencov() is called.
             % Else, sldvrun() is called.
             deltacov    = [];
             newsldvdata = [];
             refkey      = struct([]);
             
             % This takes care of cumulative collection of coverage data
             % for refH  
             totalcov = collectTotalCoverage(obj);
             % Note that if there is no coverage information available for
             % refH, totalcov could be null.
             analysisOpts = getTestExtnSLDVOpts(obj, totalcov, existingSldvData, extopts);
             showUI = true;

             if ~generateDeltaCov
                 startCov = [];
                 preExtract = [];
                 customEnhancedMCDCOpts = [];
                 [status, filenames, ~, ~, fullCovAlreadyAchieved] = ...
                     sldvprivate('sldvRunAnalysis', obj.sutH, analysisOpts, showUI, startCov, preExtract, customEnhancedMCDCOpts, client);
             else
                 [status, deltacov, filenames, ~, fullCovAlreadyAchieved] = ...
                     sldvprivate('sldvgencovImpl', obj.mdlH, analysisOpts, showUI, totalcov, client);
             end

             if fullCovAlreadyAchieved
                 return;
             end

             if status && ~isempty(filenames) && ~isempty(filenames.DataFile)
                 % Update extender needs to be updated with the new data
                 tcData      = load(filenames.DataFile);
                 if ~isempty(deltacov)
                     addCoverageData(obj, deltacov, obj.sutH);
                 end
                 newsldvdata = tcData.sldvData;
                 refkey = addTestData(obj, newsldvdata, obj.sutH);         
             end 
         end
         
         
         function testdata = getTestData(obj, refkey)
             testdata     = struct([]);
             testpresent  = isKey(obj.testdataMap, obj.sutH);
             
             if testpresent
                testdataArr  = obj.testdataMap(obj.sutH);
                if ~isempty(refkey)
                    % check consistency of refkey and refH
                    index = refkey.index;
                else
                    % last element in the array
                    index = legth(testdataArr);
                end
                testdata = testdataArr(index);
             end
         end
         
        %TBD
         function [newsldvdata, refkey] = computeTestExtension(obj, totalcov, testdata, extopts)
            newsldvdata = struct([]);
            refkey      = struct([]);
            analopts    = getTestExtnSLDVOpts(obj, totalcov, testdata.sldvdata, extopts);
            
            [status, filenames] = sldvrun(obj.sutH, analopts, true);
            
            if status
                 % Check if extender needs to be updated with the new data    
                 tcData      = load(filenames.DataFile);
                 newsldvdata = tcData.sldvData;                 
                 refkey      = addTestData(obj, newsldvdata, obj.sutH);          
             end 
            
         end
            
        
         function [newsldvdata, newrefkey] = extendTestCases(obj, refkey, extopts)
             % currently, we may have some gaps for subsystem related
             % workflows. e.g. how to get extracted model etc.
             newsldvdata = [];
             if nargin > 3        
                 % Throw error
                 return;
             end
             % set default options for coverage extension
             if nargin < 3
                extopts = obj.extopts;  
             end
             
             totalcov = collectTotalCoverage(obj);
             % we will ignore this coverage while using test suite
             % identified in opts.refkey by user.
             
             % get test data corresponding to refkey mentioned by user
             testdata = getTestData(obj, refkey);
             
             % configure sldvoptions to indicate test extension
             [newsldvdata, newrefkey] = computeTestExtension(obj, totalcov, testdata, extopts );
             
         end
         
         dlgStruct = getDialogSchema(obj);
        
         function generateTestsButtonCallback(obj, ~)
            [~, ~, filenames, ~] = obj.extendCoverage();
            if ~isempty(filenames)
                if isfield (filenames, 'DataFile') &&  ~isempty(filenames.DataFile)
                    sldvprivate('urlcall', 'export_to_sltest', filenames.DataFile, get_param(obj.sutH, 'Name'));
                end
            end
         end
         
         function setAnalysisStrategyCallback(obj, dialog)
             w = DAStudio.imDialog.getIMWidgets(dialog);
            strat = find(w, 'Tag', 'StrategyComboBox');
            obj.extopts.DVTestSuiteOptimization = strat.currentText;
         end
         
         function setMaxProcessingTimeCallback(obj, dialog)
             w = DAStudio.imDialog.getIMWidgets(dialog);
            strat = find(w, 'Tag', 'AnalysisTimeoutEditBox');
            if ~isempty(str2double(strat.Text))
                obj.extopts.DVMaxProcessTime = int16(str2double(strat.Text));
            else
                warning('Sldv:TopItOff:InvalidSettingForMaxTimeout', getString(message('Sldv:TopItOff:InvalidSettingForMaxTimeout')));
            end
         end
         
         function closeExtenderButtonCallback(~, dialog)
            delete(dialog);
         end
    end    
end

% LocalWords:  outdir TestExtendersldvData sutH HarnessDataNotforSUT testdatamap refkey covdata
% LocalWords:  totalcov

