classdef test_matlab_syntax < matlab.unittest.TestCase
  % Class to test matlab syntax issues in the present directory
  % uses checkresults() matlab function
  %
  % [+GenLib General Purpose Library+] Swiss Plasma Center EPFL Lausanne 2022. All rights reserved.
  
  % F. Felici SPC-EPFL 2020
  
  properties (TestParameter)
    FileToCheck = test_matlab_syntax.getFilesToCheck();
    MaxSeverity = {2}; % Severity 2 or higher means an MLINT 'error'
  end
  
  properties
    Warnings
    Categories
    MaxCabe = 20; % Maximum McCabe complexity, too high means code should be simplified.
  end
  
  methods (TestClassSetup)
    function buildMlintCatalog(testCase)
      % list of files to check
      [testCase.Warnings, testCase.Categories] = test_matlab_syntax.mlintCatalog();
    end
  end
  
  methods (Test)
    function checkcode(testCase,FileToCheck,MaxSeverity)
      myfile = FileToCheck;
      myfilepath = fullfile(myfile.folder,myfile.name);
      
      %%% Check code
      checkresults = checkcode(myfilepath,'-id','-cyc','-config=factory');
      
      % Optional log
      log(testCase,matlab.unittest.Verbosity.Verbose,sprintf('Checking %s',myfilepath));
      
      for ii=1:numel(checkresults)
        % scan results and flag any potential issues
        myresults = checkresults(ii);
        
        if contains(myresults.id,'CABE')
          % McCabe complexity of code - this is not a warning but an
          % optional output of the code analysis
          
          Cabe = str2double(cell2mat(regexp(myresults.message,'(.\d)\.$','match'))); % get CABE value from string
          if Cabe>testCase.MaxCabe
            log(testCase,matlab.unittest.Verbosity.Detailed,...
              sprintf('McCabe complexity exceeds limit - can this code be refactored?\n File: %s\n Msg: %s\n',...
              myfilepath,myresults.message));
          end
          
          log(testCase,matlab.unittest.Verbosity.Verbose,myresults.message);
          continue; % skip to next
        end
        % get severity of issue
        msgID = myresults.id;
        errorSeverity = testCase.Warnings.(msgID).severity;
        
        % Return log (depending on verbose setting)
        if errorSeverity ==0
          logVerbosityLevel = matlab.unittest.Verbosity.Verbose;
        elseif errorSeverity == 1
          logVerbosityLevel = matlab.unittest.Verbosity.Detailed;
        else
          logVerbosityLevel = matlab.unittest.Verbosity.Concise;
        end
        log(testCase,logVerbosityLevel,...
          sprintf('Code Check Warning:\n Message:%s\n File:%s\n Line:%d',...
          myresults.message,myfilepath,myresults.line)...
          );
        
        % test that Severity it is lower than limit
        testCase.verifyLessThan(errorSeverity,MaxSeverity, ...
          sprintf('Syntax error or other serious error detected!\n Message:%s\nFile:%s\nLine:%d',...
          myresults.message,myfilepath,myresults.line)...
          );
      end
    end
  end
  
  methods (Static)
    
    function [FilesToCheck] = getFilesToCheck()
      dirresult = dir(fullfile(pwd,'**/*.m')); % list of m files in pwd
      FilesToCheck = cell(1,numel(dirresult));
      for ii=1:numel(dirresult)
        FilesToCheck{ii} = dirresult(ii);
      end
    end
    
    function [warnings, categories] = mlintCatalog()
      % Get a list of all categories, mlint IDs, and severity rankings
      % Plucked from https://stackoverflow.com/questions/35898444/find-category-of-matlab-mlint-warning-id
      output = evalc('checkcode sum.m -allmsg');
      
      % Break each line into it's components
      lines = regexp(output, '\n', 'split').';
      pattern = '^\s*(?<id>[^\s]*)\s*(?<severity>\d*)\s*(?<message>.*?\s*$)';
      warnings = regexp(lines, pattern, 'names');
      warnings = cat(1, warnings{:});
      
      % Determine which ones are category names
      isCategory = cellfun(@isempty, {warnings.severity});
      categories = warnings(isCategory);
      
      % Fix up the category names
      pattern = '(^\s*=*\s*|\s*=*\s*$)';
      messages = {categories.message};
      categoryNames = cellfun(@(x)regexprep(x, pattern, ''), messages, 'uni', 0);
      [categories.message] = categoryNames{:};
      
      % Now pair each mlint ID with it's category
      comp = bsxfun(@gt, 1:numel(warnings), find(isCategory).');
      [category_id, ~] = find(diff(comp, [], 1) == -1);
      category_id(end+1:numel(warnings)) = numel(categories);
      
      % Assign a category field to each mlint ID
      [warnings.category] = categoryNames{category_id};
      
      category_id = num2cell(category_id);
      [warnings.category_id] = category_id{:};
      
      % Remove the categories from the warnings list
      warnings = warnings(~isCategory);
      
      % Convert warning severity to a number
      severity = num2cell(str2double({warnings.severity}));
      [warnings.severity] = severity{:};
      
      % Save just the categories
      categories = rmfield(categories, 'severity');
      
      % Convert array of structs to a struct where the MLINT ID is the field
      warnings = orderfields(cell2struct(num2cell(warnings), {warnings.id}));
    end
  end
end


