├── demo ├── Application.cfc ├── index.cfm └── Person.cfc ├── src ├── images │ ├── info.jpg │ ├── list.jpg │ ├── logo.ai │ └── logo.png └── context │ └── admin │ └── debug │ └── CodeCoverage.cfc ├── filecoverage ├── Application.cfc ├── info.cfm ├── index.cfm └── FCReporter.cfc ├── .travis.yml ├── LICENSE ├── box.json └── README.md /demo/Application.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | this.name = "CodeCoverage"; 3 | } -------------------------------------------------------------------------------- /src/images/info.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cybersonic/lucee-filecoverage-extension/HEAD/src/images/info.jpg -------------------------------------------------------------------------------- /src/images/list.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cybersonic/lucee-filecoverage-extension/HEAD/src/images/list.jpg -------------------------------------------------------------------------------- /src/images/logo.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cybersonic/lucee-filecoverage-extension/HEAD/src/images/logo.ai -------------------------------------------------------------------------------- /src/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cybersonic/lucee-filecoverage-extension/HEAD/src/images/logo.png -------------------------------------------------------------------------------- /demo/index.cfm: -------------------------------------------------------------------------------- 1 | 2 | Person = new Person(name="Mark", age=44); 3 | 4 | Person.called(); 5 | Hello #Person.getName()# #Person.getAge()# #Person.getName()# -------------------------------------------------------------------------------- /filecoverage/Application.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | this.name = "FileCoverage_" & Hash(getCurrentTemplatePath()); 3 | this.datasource = "codecoverage"; 4 | 5 | 6 | 7 | function onRequestStart(targetPage){ 8 | request.basePath = contractPath(getDirectoryFromPath(getCurrentTemplatePath())); 9 | } 10 | 11 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | install: ant 3 | script: ant build 4 | deploy: 5 | provider: releases 6 | api_key: $TOKEN 7 | file: dist/filecoverage-extension.lex 8 | name: $(date +'%Y%m%d%H%M%S')-$(git log --format=%h -1)-SNAPSHOT 9 | on: 10 | repo: cybersonic/lucee-filecoverage-extension 11 | branch: master -------------------------------------------------------------------------------- /demo/Person.cfc: -------------------------------------------------------------------------------- 1 | component accessors="true" { 2 | 3 | property name="name"; 4 | property name="age"; 5 | 6 | 7 | function uncalled(){ 8 | 9 | // Nothing here. 10 | //Nothing 11 | echo("THis method should not be called"); 12 | } 13 | 14 | function called(){ 15 | privateCalled(); 16 | } 17 | 18 | function privateCalled(){ 19 | 20 | } 21 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Mark Drew 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /box.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"Lucee File Coverage Debug Template", 3 | "version":"0.0.1.13", 4 | "author":"Mark Drew", 5 | "location":"https://github.com/cybersonic/lucee-filecoverage-extension/releases/download/0.0.1.13/filecoverage-extension.lex", 6 | "homepage":"https://github.com/cybersonic/lucee-filecoverage-extension/releases", 7 | "documentation":"", 8 | "repository":{ 9 | "type":"github", 10 | "URL":"git@github.com:cybersonic/lucee-filecoverage-extension.git" 11 | }, 12 | "bugs":"https://github.com/cybersonic/lucee-filecoverage-extension/issues", 13 | "slug":"lucee-file-coverage", 14 | "shortDescription":"This extension provides a debugging template that logs all the calls to script files as a test or QA person navigates your site. ", 15 | "description":"This extension gives you access to see which files are run by Lucee when you make a number of requests. It provides a percentage of coverage per file that your application uses, allowing you to know what you should remove or focus efforts on, such as where you should add tests.", 16 | "instructions":" ", 17 | "changelog":"", 18 | "type":"lucee-extensions", 19 | "keywords":"lucee extension debugging file code coverage", 20 | "private":false, 21 | "projectURL":"https://github.com/cybersonic/lucee-filecoverage-extension", 22 | "license":[ 23 | { 24 | "type":"MIT License", 25 | "URL":"https://raw.githubusercontent.com/cybersonic/lucee-filecoverage-extension/master/LICENSE" 26 | } 27 | ], 28 | "contributors":[], 29 | "dependencies":{ 30 | 31 | }, 32 | "devDependencies":{}, 33 | "installPaths":{}, 34 | "scripts":{}, 35 | "ignore":[ 36 | "**/.*", 37 | "test", 38 | "tests" 39 | ] 40 | } -------------------------------------------------------------------------------- /filecoverage/info.cfm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | File Coverage 5 | 6 | 7 | 8 | 9 | 10 | reporter = new FCReporter(); 11 | report = reporter.getInfoForFile(url.dir); 12 | 13 | pathSeparator = reporter.getPathSeparator(); 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 |

File Coverage

24 |

25 | PATH: #URL.DIR# 26 |

27 | 28 |
29 |
30 |
31 | Back 32 | 33 |
34 |
35 |
36 | 37 | 38 | 39 |
40 |
41 | #item# 42 |
43 |
44 | #report.summary[item]# 45 |
46 |
47 |
48 | 49 | 50 |
51 |
52 | Methods 53 |
54 |
55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 |
HitsName
#report.summary.methods[method]##method#()
79 | 80 |
81 |
82 | 83 |
84 | 85 |
86 |
87 | Source 88 |
89 |
90 | #htmlCodeFormat(report.summary.source)# 91 |
92 |
93 | 94 | 95 | 100 |
101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /filecoverage/index.cfm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | File Coverage 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | reporter = new FCReporter(); 18 | if(url.action EQ "delete"){ 19 | reporter.deleteCoverage(); 20 | } 21 | 22 | report = reporter.getReportForDirectory(url.dir); 23 | cover = reporter.getCoverageForDirectory(url.dir,true); 24 | 25 | pathSeparator = reporter.getPathSeparator(); 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 |

File Coverage

35 |

36 | PATH: #URL.DIR# 37 |

38 | 39 |
40 |
41 |
42 | Back 43 | Delete Report 44 |
45 |
46 |
47 | 48 |
49 |
50 | #decimalFormat(100/cover.total*cover.accessed)#% Coverage 51 | 52 |
53 |
54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 |
HitsName
71 | #directory.hits##directory.name#
#item.hits##item.name#
92 |
93 |
94 | 95 |
96 | 97 |
98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lucee File Coverage Extension 2 | [![Build Status](https://travis-ci.com/cybersonic/lucee-filecoverage-extension.svg?branch=master)](https://travis-ci.com/cybersonic/lucee-filecoverage-extension) 3 | This extension provides a debugging template that logs all the calls to script files as a user navigates your site. It then allows you to browse which files have been used and more importantly which have not 4 | 5 | ## Getting Started 6 | 7 | (We are hoping this will go onto the lucee extension store in the future). 8 | 9 | 10 | Check out the installation guide. At the moment this is the main way of installing it until we get this everything ready to be added to the Lucee Extension "store". 11 | 12 | I would love to be able to actually add this to http://forgebox.io too. 13 | 14 | 15 | 16 | ### Prerequisites 17 | 18 | - Apache Ant to build the project (it is running a simple build.xml file) 19 | - Lucee 5.x and greater. No ACF implementation yet. 20 | - Currently it is also using the H2 database extension. Which can be installed fropm the Extension:Applications section of the admin. 21 | 22 | 23 | ### Installing 24 | 25 | First you need to build the project, simply go into the root and run: 26 | ``` 27 | ant 28 | ``` 29 | 30 | This will create a file in the `dist` folder called `filecoverage-extension-X.X.X.X.lex`. This extension is meant to deployed to a *web context* (versus a server context) as it installs the reporting application into your webroot in a folder called `/filecoverage` so you can access it as http://localhost/filecoverage. 31 | 32 | 1. Go to the Lucee web administrator - http://localhost/lucee/admin/web.cfm and log in 33 | 1. Click on `Applications` under *Extension* 34 | 1. Scroll to the bottom to where it says "Upload new extension (experimental)" and choose the `filecoverage-extension-X.X.X.X.lex` we created and click *Upload* 35 | 1. Since we need to use a database to store the captured data, instal the `H2` database 36 | 1. Create a datasource called `codecoverage` using the H2 Database Engine in Embedded Mode 37 | 1. Put a path in the path section (for example "db") and click save 38 | 1. Under *Debugging:Settings* click "Yes" to enable debugging. 39 | 1. Under *Debugging:Templates* enter a label such as "CodeCoverage" and select the "File Coverage" template type and click create 40 | 1. In the Datasource Name field enter 'codecoverage' and click submit ( you can also then limit to which IP ranges you want this debugging template to work with) 41 | 42 | The site is now ready for testing. You just need to browse it. The next part is viewing the results. 43 | 44 | 45 | ### Reports 46 | 47 | At the moment this is pretty rough and ready and you just need to go to http://localhost/filecoverage to view how many hits each file gets as well as some information on which functions/methods are used in a CFC. 48 | 49 | ![Listing of files and directories](src/images/list.jpg) 50 | 51 | ![Details about a CFC](src/images/info.jpg) 52 | 53 | 54 | 55 | 56 | 78 | ## Deployment 79 | 80 | This extension should *NOT* be deployed on a live system. It's meant to be used as part of an investigation or your testing process. DO. NOT. RUN. ON. A. LIVE. SYSTEM. 81 | You have been warned. 82 | 83 | 84 | 88 | ## Versioning 89 | 90 | 91 | We use [SemVer](http://semver.org/) for versioning. 92 | 93 | ## Authors 94 | 95 | * **Mark Drew** - *Initial work* - [cybersonic](https://github.com/cybersonic) 96 | 97 | 98 | 99 | ## License 100 | 101 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details 102 | -------------------------------------------------------------------------------- /src/context/admin/debug/CodeCoverage.cfc: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | variables.tablename = "lucee_filecoverage_extension"; 5 | fields=array( 6 | group("Settings","Main Settings of the plugin",3) 7 | , field("Datasource Name", "dsn","", true,"" , "text100" ) 8 | 9 | 10 | ); 11 | 12 | string function getLabel(){ 13 | return "File Coverage"; 14 | } 15 | 16 | string function getDescription(){ 17 | return "Template to log which script files and components are used by each request."; 18 | } 19 | 20 | string function getid(){ 21 | return "lucee-codecoverage"; 22 | } 23 | 24 | void function onBeforeUpdate(struct custom){ 25 | // throwWhenNotNumeric(custom,"minimal"); 26 | // throwWhenNotNumeric(custom,"highlight"); 27 | var createDBTable = queryExecute( 28 | sql:"CREATE CACHED TABLE IF NOT EXISTS PUBLIC.#variables.tablename#( 29 | ID BIGINT auto_increment, 30 | SRC VARCHAR(500), 31 | FILEPATH VARCHAR(500), 32 | METHOD VARCHAR(255), 33 | COUNT INT, 34 | MIN INT, 35 | MAX INT, 36 | AVG INT, 37 | APP INT, 38 | LOAD INT, 39 | QUERY INT, 40 | TOTAL INT, 41 | HASH VARCHAR(100) 42 | ) 43 | ", 44 | options:{ 45 | datasource:arguments.custom.dsn 46 | } 47 | ); 48 | } 49 | 50 | private void function throwWhenEmpty(struct custom, string name){ 51 | if(!structKeyExists(custom,name) or len(trim(custom[name])) EQ 0) 52 | throw "value for ["&name&"] is not defined"; 53 | } 54 | 55 | private void function throwWhenNotNumeric(struct custom, string name){ 56 | throwWhenEmpty(arguments.custom, arguments.name); 57 | if(!isNumeric(trim(arguments.custom[arguments.name]))) 58 | throw "value for [" & arguments.name & "] must be numeric"; 59 | } 60 | 61 | 62 | private boolean function DoesTableExist(TableName,DSN){ 63 | dbinfo name="ALLTABLES" type="Tables" datasource="#arguments.DSN#"; 64 | var results = queryExecute( 65 | sql:"SELECT TABLE_NAME FROM ALLTABLES WHERE TABLE_NAME = ?", 66 | params: [TableName], 67 | options:{ 68 | "dbtype": "Query" 69 | } 70 | ) 71 | 72 | if(results.recordcount){ 73 | return true; 74 | } 75 | 76 | 77 | return false; 78 | } 79 | 80 | 81 | function output(custom,debugging,context){ 82 | 83 | 84 | loop query="debugging.pages"{ 85 | var filepath = ListFirst(src,"$"); 86 | var method = ListLast(src,"$"); 87 | 88 | method = method EQ filepath ? "" : method; 89 | 90 | var ins = queryExecute( 91 | sql:"INSERT INTO #variables.tablename# 92 | (SRC,FILEPATH,METHOD,COUNT,MIN,MAX,AVG,APP,LOAD,QUERY,TOTAL,HASH) 93 | 94 | VALUES(:src,:filepath,:method,:count,:min,:max,:avg,:app,:load,:query,:total,:hash)", 95 | 96 | params:{src:src,filepath:filepath,method:method,count:count,min:min,max:max,avg:avg,app:app,load:load,query:query,total:total,hash:hash(src)}, 97 | 98 | options:{ 99 | datasource:arguments.custom.dsn 100 | }); 101 | } 102 | } 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | function unitFormat( string unit, numeric time, boolean prettify=false ) { 120 | if ( !arguments.prettify ) { 121 | return NumberFormat( arguments.time / 1000000, ",0.000" ); 122 | } 123 | 124 | // display 0 digits right to the point when more or equal to 100ms 125 | if ( arguments.time >= 100000000 ) 126 | return int( arguments.time / 1000000 ); 127 | 128 | // display 1 digit right to the point when more or equal to 10ms 129 | if ( arguments.time >= 10000000 ) 130 | return ( int( arguments.time / 100000 ) / 10 ); 131 | 132 | // display 2 digits right to the point when more or equal to 1ms 133 | if ( arguments.time >= 1000000 ) 134 | return ( int( arguments.time / 10000 ) / 100 ); 135 | 136 | // display 3 digits right to the point 137 | return ( int( arguments.time / 1000 ) / 1000 ); 138 | 139 | } 140 | 141 | 142 | function byteFormat( numeric size ) { 143 | 144 | var values = [ [ 1099511627776, 'TB' ], [ 1073741824, 'GB' ], [ 1048576, 'MB' ], [ 1024, 'KB' ] ]; 145 | 146 | for ( var i in values ) { 147 | 148 | if ( arguments.size >= i[ 1 ] ) 149 | return numberFormat( arguments.size / i[ 1 ], '9.99' ) & i[ 2 ]; 150 | } 151 | 152 | return arguments.size & 'B'; 153 | } 154 | 155 | /** reads the file contents and writes it to the output stream */ 156 | function includeInline(filename) cachedWithin=createTimeSpan(0,1,0,0) { 157 | 158 | echo(fileRead(expandPath(arguments.filename))); 159 | } 160 | 161 | function getJavaVersion() { 162 | var verArr=listToArray(server.java.version,'.'); 163 | if(verArr[1]>2) return verArr[1]; 164 | return verArr[2]; 165 | } 166 | 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /filecoverage/FCReporter.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | 3 | variables.tablename = "lucee_filecoverage_extension"; 4 | variables.extensionfilter = "*.cf*"; 5 | 6 | variables.pathSeparator = getPathSeparator(); 7 | 8 | //TODO: Add ignored foldders etc. 9 | 10 | public any function getCoverageForDirectory(String path, boolean recurse=false){ 11 | 12 | var delimiter = Right(Path,1) EQ variables.pathSeparator? "" : variables.pathSeparator; 13 | 14 | 15 | 16 | var files = DirectoryList(path,recurse,"query",variables.extensionfilter,"Name"); 17 | 18 | 19 | 20 | var results = { 21 | total: files.recordcount, 22 | accessed: 0 23 | } 24 | 25 | 26 | loop query="files"{ 27 | //See if any of these have hits! 28 | var hitsForThisScript = getTotalHits(directory & variables.pathSeparator & name); 29 | 30 | if(hitsForThisScript){ 31 | results.accessed++; 32 | } 33 | 34 | } 35 | return results; 36 | } 37 | 38 | 39 | public any function getReportForDirectory(String path, boolean recurse=false){ 40 | 41 | 42 | //Directories need to be calculated differently, as we get a roll up of all files accesed underneath them rather than an exact match. 43 | 44 | var ret_files = []; 45 | var ret_dirs = []; 46 | 47 | var files = DirectoryList(path,recurse,"query",variables.extensionfilter,"name","all") 48 | 49 | for(file in files){ 50 | 51 | 52 | file["hits"] = getTotalHits(file.directory & variables.pathSeparator & file.name); 53 | ret_files.append(file); 54 | } 55 | 56 | 57 | 58 | var directories = DirectoryList(path,recurse, "query", "*","name", "dir"); 59 | 60 | for(dir in directories){ 61 | 62 | dir["hits"] = findTotalHits(dir.directory & variables.pathSeparator & dir.name); 63 | ret_dirs.append(dir); 64 | } 65 | 66 | 67 | 68 | return { 69 | files:ret_files, 70 | directories:ret_dirs 71 | } 72 | } 73 | 74 | function getInfoForFile(PathToFind){ 75 | var raw = queryExecute(sql:"SELECT * FROM #variables.tablename# WHERE FILEPATH = '#PathToFind#'"); 76 | 77 | var type = ListLast(PathToFind,".") EQ "cfc" ? "Component" : "Script"; 78 | 79 | var methods = {}; 80 | //Get metadata 81 | if(type EQ "Component"){ 82 | 83 | var dotPath = getDotPathFromPath(PathToFind); 84 | var componentMetaData = getComponentMetadata(dotPath); 85 | 86 | for(func in componentMetaData.functions){ 87 | methods[func.name] = 0; 88 | } 89 | 90 | raw.each(function(item, index, query){ 91 | 92 | if(Len(Trim(item.method))){ 93 | methods[item.method] = methods[item.method]+item.count; 94 | } 95 | 96 | }); 97 | 98 | } 99 | 100 | 101 | 102 | 103 | 104 | var count = 0; 105 | if (raw.recordCount) { 106 | count = raw.reduce(function(hits=0,cols,index,query){ 107 | return hits + cols.count; 108 | }); 109 | } 110 | 111 | 112 | return { 113 | raw: raw, 114 | summary: { 115 | name: getFileFromPath(PathToFind), 116 | directory: getDirectoryFromPath(PathToFind), 117 | type: type, 118 | methods: methods, 119 | hits: count, 120 | source: FileRead(PathToFind) 121 | } 122 | } 123 | } 124 | 125 | 126 | function getDotPathFromPath(PathToFind){ 127 | var dotPath = contractPath(PathToFind); 128 | dotPath = listDeleteAt(dotPath, ListLen(dotPath, "."),"."); 129 | dotPath = listTrim(dotPath,variables.pathSeparator); 130 | dotPath = Replace(dotPath, variables.pathSeparator, ".", "all"); 131 | 132 | return dotPath; 133 | } 134 | 135 | //Exact match for a file search 136 | function getTotalHits(PathToFind){ 137 | 138 | var found = queryExecute(sql:"SELECT SUM(count) AS hits FROM #variables.tablename# WHERE FILEPATH = '#PathToFind#'"); 139 | 140 | 141 | if(!isNumeric(found.hits)){ 142 | return 0; 143 | } 144 | return found.hits; 145 | } 146 | 147 | //Finds all the matches of path% rather than an exact match 148 | function findTotalHits(PathToFind){ 149 | 150 | var found = queryExecute(sql:"SELECT SUM(count) AS hits FROM #variables.tablename# WHERE FILEPATH LIKE '#PathToFind#%'"); 151 | 152 | if(!isNumeric(found.hits)){ 153 | return 0; 154 | } 155 | return found.hits; 156 | } 157 | 158 | function deleteCoverage(){ 159 | var found = queryExecute(sql:"DELETE FROM #variables.tablename#"); 160 | writeDump(getAll()); 161 | 162 | } 163 | 164 | function createCoverageTable(){ 165 | var createDBTable = queryExecute( 166 | sql:"DROP TABLE IF EXISTS PUBLIC.#variables.tablename#" 167 | ); 168 | 169 | var createDBTable = queryExecute( 170 | sql:"CREATE CACHED TABLE IF NOT EXISTS PUBLIC.#variables.tablename#( 171 | ID BIGINT auto_increment, 172 | SRC VARCHAR(500), 173 | FILEPATH VARCHAR(500), 174 | METHOD VARCHAR(255), 175 | COUNT INT, 176 | MIN INT, 177 | MAX INT, 178 | AVG INT, 179 | APP INT, 180 | LOAD INT, 181 | QUERY INT, 182 | TOTAL INT, 183 | HASH VARCHAR(100) 184 | ) 185 | " 186 | ); 187 | } 188 | 189 | function getAll(){ 190 | return queryExecute(sql:"SELECT * FROM #variables.tablename#"); 191 | } 192 | 193 | function getPathSeparator(){ 194 | var File = CreateObject("java", "java.io.File"); 195 | return File.separator; 196 | } 197 | 198 | } 199 | --------------------------------------------------------------------------------