├── 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 |
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 | | Hits |
60 | Name |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | | #report.summary.methods[method]# |
71 | #method#() |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
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 |
47 |
48 |
49 |
50 | #decimalFormat(100/cover.total*cover.accessed)#% Coverage
51 |
52 |
53 |
54 |
55 |
56 |
57 | |
58 | Hits |
59 | Name |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | |
71 | |
72 | #directory.hits# |
73 | #directory.name# |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | |
83 | #item.hits# |
84 | #item.name# |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
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 | [](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 | 
50 |
51 | 
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 |
--------------------------------------------------------------------------------