Skip to content

Commit

Permalink
Implement Unit Testing for FunctionList submissions to UDL Collection
Browse files Browse the repository at this point in the history
- Use main app FunctionList UnitTest as the basis, tweaked for the UDL-requirements and collection hierarchy
- Includes JSON update required to make existing FunctinoList ofr LSL_byKimpaTammas pass
- Update CONTRIBUTING.md to include instructions for the UnitTest portion of the submission

(Implemented over many commits in my branch, but squash to a single commit and force-push before creating PR)

closes #255
  • Loading branch information
pryrt committed Jun 24, 2024
1 parent fc1ee6b commit 05fb033
Show file tree
Hide file tree
Showing 11 changed files with 508 additions and 14 deletions.
34 changes: 23 additions & 11 deletions .github/workflows/CI_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@ name: CI_build
on: [push, pull_request, workflow_dispatch]

jobs:
build:

runs-on: windows-2022

validate:
runs-on: windows-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
Expand All @@ -23,14 +21,28 @@ jobs:
working-directory: .
run: python .validators\validator_json.py

- uses: stefanzweifel/git-auto-commit-action@v5
- name: Rebuild Markdown
uses: stefanzweifel/git-auto-commit-action@v5
if: contains('push workflow_dispatch', github.event_name)
with:
commit_message: Automatically re-build udl-list.md

#- name: Archive artifacts udl markdown
# uses: actions/upload-artifact@v3
# with:
# name: markdown_udl_list
# path: udl-list.md

unitTest:
runs-on: windows-latest
needs: validate
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Install Notepad++
uses: crazy-max/ghaction-chocolatey@v3
with:
args: install -y notepadplusplus
- name: Run FunctionList Unit Tests
working-directory: .\Test
run: |
$PowerEditorSource = "C:\Program Files\Notepad++\"
$PowerEditorLocal = ".\PowerEditor"
Copy-Item "$PowerEditorSource" -Destination "$PowerEditorLocal\bin" -Recurse -Force
New-Item "$PowerEditorLocal\bin\doLocalConf.xml" > $nul
New-Item "$PowerEditorLocal\bin\userDefineLangs" -ItemType Directory -ea 0 > $nul
python doUnitTests.py $PowerEditorLocal\bin
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
PowerEditor
**/unitTest
**/unitTest.result.json
38 changes: 37 additions & 1 deletion .validators/validator_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ def parse(filename):
fl_link = str(udl["functionList"]) + ".xml"
fl_link_abs = Path(os.path.join(os.getcwd(),"functionList", fl_link))

if fl_link[0:4] == "http":
if len(fl_link)>4 and fl_link[0:4] == "http":
try:
response = requests.get(fl_link)
print(f' + also confirmed functionList URL: {fl_link}')
Expand All @@ -400,6 +400,42 @@ def parse(filename):
else:
print(f' + also confirmed "functionList/{fl_link}"')

sfile = None
if not 'sample' in udl: # doesn't exist
post_error(f'{udl["display-name"]}: functionList file requires sample filefilename="functionList/{fl_link}"')
elif not udl['sample']: # exists but not true
post_error(f'{udl["display-name"]}: functionList file requires sample filefilename="functionList/{fl_link}"')
elif str(udl['sample']) == 'True':
sfile = udl["id-name"]
else:
sfile = str(udl['sample'])

if sfile:
# verify sample UDL file exists
spath = Path(os.path.join(os.getcwd(),"UDL-samples", sfile))
if len(sfile)>4 and sfile[0:4] == 'http':
try:
response = requests.get(sfile)
print(f' + also confirmed sample-file URL: {sfile}')
except requests.exceptions.RequestException as e:
post_error(str(e))
elif not spath.exists():
post_error(f'{udl["display-name"]}: functionList UDL-sample file missing from repo: JSON id-name expects it at filename="UDL-samples/{sfile}"')
else:
print(f' + also confirmed "UDL-samples/{sfile}"')

# verify Test directory exists for this UDL+FL
testDir = Path(os.path.join(os.getcwd(), "Test", "functionList", udl['id-name']))
if not testDir.exists():
post_error(f'{udl["display-name"]}: functionList Test directory missing from repo: JSON id-name expects it at filename="{testDir}"')
continue

# verify expected-results file exists for this UDL+FL
expectFile = Path(os.path.join(os.getcwd(), "Test", "functionList", udl['id-name'], "unitTest.expected.result"))
if not expectFile.exists():
post_error(f'{udl["display-name"]}: functionList Test directory missing expected results: JSON id-name expects it at filename="{expectFile}"')
continue

return udlfile


Expand Down
27 changes: 25 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ To be accepted, your submission _must_ meet the following **requirement**s and _
5. **recommendation**: if your UDL file only contains one language, the `display-name` attribute in the JSON file (described below) should have the same value as the `<UserLang name="...">` inside your definition file. This will keep the name in the **Language** menu the same as the name that was shown in the download tool (once it is developed and released).
6. **recommendation**: in your Pull Request, please provide a link to a public description of the language your UDL is based on (which will help to establish the general-interest nature of the UDL), as well as a link to an example file in that language (so that the UDL can be verified as functional).
* If you have an example file, you can upload it to the `UDL-samples` folder of the repository. Please have this file use the same name as your UDL definition file, but with the appropriate file extension, rather than `.xml`. Example: `UDLs\STL_udl.byPryrt.xml` would have a corresponding example file `UDL-samples\STL_udl.byPryrt.stl`.
* **requirement**: if you have included a functionList definition, it must also include a sample file.
7. **recommendation**: if you have also created an [autoCompletion file](https://npp-user-manual.org/docs/auto-completion/) for your UDL, you may add it in the `autoCompletion` folder before you submit your PR, using a similar naming scheme to the UDL's XML filename.
8. **recommendation**: if you have also created a [functionList definition](https://npp-user-manual.org/docs/function-list/) for your UDL, you may add it in the `functionList` folder before you submit your PR, using a similar naming scheme to the UDL's XML filename.

Expand All @@ -47,10 +48,22 @@ When you make a submission, you should edit the [udl-list.json](https://github.c
- If it is `false` or not supplied, then it indicates there is no functionList definition file in the submission.
- The `functionListAuthor` attribute should be set to the name of the author of the functionList definition, if it's a different author than the UDL.
- The `functionListAuthor` is set in order to give proper credit to both, even if they are made to work together.
- The `sample` attribute maybe be included if you have submitted a file in the `UDL-samples\` directory.
- **Requirement**: if you have submitted a `functionList`, you _MUST_ also submit a `sample` file.
- If the attribute value is `true`, then the filename in `UDL-samples\` directory must exactly match the `id-name` (so the file will have no extension).
- If the attribute is a string, then the filename in `UDL-samples\` directory must exactly match that string.
- If the attribute value is `false` or the attribute is not supplied, then you are not mapping this UDL to a sample file. This will flag as an error if a `functionList` is defined for this UDL.

## Validation
### Function List submission requirements

The maintenance team will be checking the UDL definition file, `udl-list.json`, and the autoCompletion definition (if supplied) for conformance to these requirements. By submitting the Pull Request, you are giving permission for edits to help it match the requirements, and you may be asked to make changes yourself. If you need help with the JSON, please ask for help in the Pull Request, and be willing and available to answer questions for clarifications so that you can be helped.
If you are including a functionList definition, make sure you also include a sample file in the `UDL-samples\` directory.

Your submission must also include some Unit Test information, similar to the [Function List Unit Test requirements in the User Manual](https://npp-user-manual.org/docs/function-list/#unit-tests):

0. Make sure that your UDL and FunctionList definition are working in your local Notepad++, showing the right functions (and classes) in the Function List panel for your sample file.
1. Run `notepad++ -multiInst -nosession -export=functionList -udl="<UDL Name>" "<SampleFilePath>"` , which will create `unitTest.result.json` in the same directory as your example file
2. Create `Test\functionList\<DirectoryName>\` in your repo, where `<DirectoryName>` must match the `id-name` from the JSON, exactly.
3. Copy `unitTest.result.json` to `Test\functionList\unitTest.expected.result`

## HOW TO Submit Pull Request

Expand All @@ -74,3 +87,13 @@ Since many contributors are not GitHub experts, we have added in this section to
- fill out your description for the PR, and submit the PR

The same PR must contain the UDL XML file, the edits to `udl-list.json` (and optionally, the autoCompletion file); a PR that adds a new UDL without updating the JSON, or adds a new autoCompletion without updating the JSON, will be immediately rejected, even if you were planning to submit the JSON edit later. Submit it all in one PR, please.

## Validation and Acceptance or Rejection

By submitting the Pull Request, you are giving permission for the maintenance team audit your submission, to make any edits they deem necessary or helpful to allow it to match the requirements, though you are expected to take the lead in making changes yourself. If you need help with the JSON, please ask for help in the Pull Request, and be willing and available to answer questions for clarifications so that you can be helped.

There will be automatic validation and verification of some of these requirements, which will generally take less than 5 minutes after your PR submission. Always check the status of this automatic validation promptly, and fix any problems found or ask questions in the PR if you don't understand the resulting errors. The maintenance team may ignore your submission as long as it has validation errors, though they might ping you if your errors are unresolved after some time. If you don't understand the errors, you may ask questions in the PR conversation.

If you do not resolve the errors (or other fixes requested by the maintenance team) or at least ask for help, the maintenance team is allowed to close a PR after a week without you resolving the errors (or a week of you not replying to their most recent comment, if there has been a conversation regarding the errors).

The maintenance team have the decision power as to whether or not a given PR is accepted or rejected. If it is rejected for reasons other than validation errors or failure, they must explain their reasoning in the PR conversation.
179 changes: 179 additions & 0 deletions Test/doUnitTests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
#!/usr/local/bin/python3

import json
import os
import io
import sys
import shutil
import inspect
from timeit import default_timer as timer
import subprocess

api_url = os.environ.get('APPVEYOR_API_URL')
has_error = False

bin_dir = sys.argv[1] if sys.argv[1] else './PowerEditor/bin'
npp = os.path.join(bin_dir, 'notepad++.exe')

def post_error(message):
global has_error

has_error = True

message = {
"message": message,
"category": "error",
"details": ""
}

if api_url:
requests.post(api_url + "api/build/messages", json=message)
else:
from pprint import pprint
pprint(message)

def json_to_unitTest_launcher():
udlfile = json.loads(open("../udl-list.json", encoding="utf8").read())

# generate a map to determine which ids have ac, fl, and/or udls
print("\nLook for functionList definitions in %s" % udlfile["name"])
for udl in udlfile["UDLs"]:
id_str = udl["id-name"]

# print("- %s" % id_str)

if 'functionList' in udl:
fl = udl['functionList']
if not fl: next # do nothing if it's non-true


print("+ found functionList for %s" % udl['id-name'])
#print(json.dumps({"UDL+FL Info": udl}, sort_keys=True, indent=2, separators=(',',':')))

if fl == True:
fl = id_str

ufile = os.path.join('..', 'UDLs', id_str + ".xml")
if not os.path.exists(ufile):
ufile = udl['repository']
if not os.path.exists(ufile) and not(len(ufile)>4 and ufile[0:4] == 'http'):
post_error("Could not resolve %s" % ufile)

umap = { 'id': udl['id-name'], 'display': udl['display-name'] }

umap['udl'] = {
'src': ufile,
'dst': os.path.join(bin_dir, "userDefineLangs", id_str + ".xml")
}

umap['om'] = {
'dst': os.path.join(bin_dir, "functionList", "overrideMap.xml")
}

srcFl = fl
if not(len(fl)>4 and fl[0:4] == 'http'):
srcFl = os.path.join('..', 'functionList', fl + ".xml")

umap['fl'] = {
'src': srcFl,
'dst': os.path.join(bin_dir, "functionList", fl + ".xml")
}

smpFile = udl['sample']
umap['sample'] = {
'dst': os.path.join('functionList', id_str, 'unitTest')
}
if len(smpFile)>4 and smpFile[0:4]=='http':
post_error("TODO: don't know how to download %s yet" % (smpFile))
umap['sample']['src'] = None # TODO: the path to the file saved from download
elif smpFile:
umap['sample']['src'] = os.path.join('..', 'UDL-samples', smpFile)

umap['output'] = {
'exp': os.path.join('functionList', id_str, 'unitTest.expected.result'),
'got': os.path.join('functionList', id_str, 'unitTest.result.json')
}


#print(json.dumps(umap, sort_keys=True, indent=2, separators=(',',':')))

for k in ('udl', 'fl', 'sample'):
src = umap[k]['src']
dst = umap[k]['dst']
if os.path.exists(src):
print(" + Copy %s to %s" % (src, dst))
shutil.copy(src, dst)
elif len(src)>4 and src[0:4]=='http':
post_error("TODO: don't know how to download %s to %s" % (src, dst))
src = None # TODO: the path to the file saved from download
else:
post_error("Could not copy %s to %s" % (src, dst))

omTxt = """<?xml version="1.0" encoding="UTF-8" ?>
<NotepadPlus>
<functionList>
<associationMap>
<association id="%s" userDefinedLangName="%s"/>
</associationMap>
</functionList>
</NotepadPlus>
""" % (id_str + '.xml', udl['display-name'])
print(" + Generate %s" % (umap['om']['dst']))
with open(umap['om']['dst'], 'w') as f:
f.write(inspect.cleandoc(omTxt))

if not has_error:
one_err = run_unit_test(umap)

# delete generated files
for k in ('udl', 'fl', 'om', 'sample'):
dst = umap[k]['dst']
if os.path.exists(dst):
print(" - Remove %s" % (dst))
os.remove(dst)

if not one_err:
dst = umap['output']['got']
if os.path.exists(dst):
print(" - Remove %s" % (dst))
os.remove(dst)

def run_unit_test(umap):
"""Launch notepad++.exe with -export=functionList and process the output"""
t0 = timer()
print(" * Running unit test on %s:" % (umap['id']))
# print(json.dumps(umap, sort_keys=True, indent=2, separators=(',',':')))
cmd_str = '"%s" -multiInst -nosession -export=functionList -udl="%s" "%s"' % (npp, umap['display'], umap['sample']['dst'])
cmpl = subprocess.run(cmd_str)
print(" => exported functionList in %.1fms, returning %d" % ( (timer()-t0)*1000, cmpl.returncode))
try:
with open(umap['output']['exp']) as f:
exp = f.read().replace('\r\n', '\n').rstrip('\n')
with open(umap['output']['got']) as f:
got = f.read().replace('\r\n', '\n').rstrip('\n')

if got == exp:
print(" => compare OK")
return False
else:
print(" => compare MISMATCH")
post_error("functionList export got:\n%s\nvs expected:\n%s" % (got, exp))
except Error as e:
post_error(" => error while comparing %s to %s: %s" % (umap['output']['got'], umap['output']['exp'], str(e)))

return True

# verify it finds notepad++.exe
print("npp = %s" % npp)
if not os.path.exists(npp):
post_error("could not find %s" % npp)
sys.exit(-2)

# copy the files to the right place for FunctionList UnitTesting
json_to_unitTest_launcher()


if has_error:
sys.exit(-2)
else:
sys.exit()
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"leaves":["next_line","doMenu","mainMenu","posMenu","rotateMenu","init","state_entry","touch_start","on_rez","timer","listen","changed","run_time_permissions","dataserver"],"root":"unitTest"}
15 changes: 15 additions & 0 deletions Test/localUnitTestLauncher.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
cd Test

$PowerEditorSource = "c:\Program Files\Notepad++\"
$PowerEditorLocal = ".\PowerEditor"

if ( Test-Path $PowerEditorLocal ) {
Remove-Item -Recurse -Force $PowerEditorLocal
}

Copy-Item "$PowerEditorSource" -Destination "$PowerEditorLocal\bin" -Recurse -Force
New-Item "$PowerEditorLocal\bin\doLocalConf.xml" > $nul
New-Item "$PowerEditorLocal\bin\userDefineLangs" -ItemType Directory -ea 0 > $nul
python doUnitTests.py $PowerEditorLocal\bin

cd ..
Loading

0 comments on commit 05fb033

Please sign in to comment.