mirror of
https://github.com/GrammaticalFramework/gf-core.git
synced 2026-05-20 00:22:51 -06:00
Add proper tests, exception handling, implement getAbstractName
This commit is contained in:
8
.github/workflows/build-majestic.yml
vendored
8
.github/workflows/build-majestic.yml
vendored
@@ -56,3 +56,11 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
pip install pytest
|
pip install pytest
|
||||||
pytest
|
pytest
|
||||||
|
|
||||||
|
- name: Run JavaScript testsuite
|
||||||
|
working-directory: ./src/runtime/python
|
||||||
|
env:
|
||||||
|
LD_LIBRARY_PATH: /usr/local/lib
|
||||||
|
run: |
|
||||||
|
npm ci
|
||||||
|
npm run test
|
||||||
|
|||||||
49
src/runtime/javascript/errno.ts
Normal file
49
src/runtime/javascript/errno.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
interface ErrorInfo {
|
||||||
|
errno: number
|
||||||
|
code: string
|
||||||
|
description: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// taken from /usr/include/asm-generic/errno-base.h
|
||||||
|
const errs: ErrorInfo[] = [
|
||||||
|
{ code: 'EPERM', errno: 1, description: 'Operation not permitted' },
|
||||||
|
{ code: 'ENOENT', errno: 2, description: 'No such file or directory' },
|
||||||
|
{ code: 'ESRCH', errno: 3, description: 'No such process' },
|
||||||
|
{ code: 'EINTR', errno: 4, description: 'Interrupted system call' },
|
||||||
|
{ code: 'EIO', errno: 5, description: 'I/O error' },
|
||||||
|
{ code: 'ENXIO', errno: 6, description: 'No such device or address' },
|
||||||
|
{ code: 'E2BIG', errno: 7, description: 'Argument list too long' },
|
||||||
|
{ code: 'ENOEXEC', errno: 8, description: 'Exec format error' },
|
||||||
|
{ code: 'EBADF', errno: 9, description: 'Bad file number' },
|
||||||
|
{ code: 'ECHILD', errno: 10, description: 'No child processes' },
|
||||||
|
{ code: 'EAGAIN', errno: 11, description: 'Try again' },
|
||||||
|
{ code: 'ENOMEM', errno: 12, description: 'Out of memory' },
|
||||||
|
{ code: 'EACCES', errno: 13, description: 'Permission denied' },
|
||||||
|
{ code: 'EFAULT', errno: 14, description: 'Bad address' },
|
||||||
|
{ code: 'ENOTBLK', errno: 15, description: 'Block device required' },
|
||||||
|
{ code: 'EBUSY', errno: 16, description: 'Device or resource busy' },
|
||||||
|
{ code: 'EEXIST', errno: 17, description: 'File exists' },
|
||||||
|
{ code: 'EXDEV', errno: 18, description: 'Cross-device link' },
|
||||||
|
{ code: 'ENODEV', errno: 19, description: 'No such device' },
|
||||||
|
{ code: 'ENOTDIR', errno: 20, description: 'Not a directory' },
|
||||||
|
{ code: 'EISDIR', errno: 21, description: 'Is a directory' },
|
||||||
|
{ code: 'EINVAL', errno: 22, description: 'Invalid argument' },
|
||||||
|
{ code: 'ENFILE', errno: 23, description: 'File table overflow' },
|
||||||
|
{ code: 'EMFILE', errno: 24, description: 'Too many open files' },
|
||||||
|
{ code: 'ENOTTY', errno: 25, description: 'Not a typewriter' },
|
||||||
|
{ code: 'ETXTBSY', errno: 26, description: 'Text file busy' },
|
||||||
|
{ code: 'EFBIG', errno: 27, description: 'File too large' },
|
||||||
|
{ code: 'ENOSPC', errno: 28, description: 'No space left on device' },
|
||||||
|
{ code: 'ESPIPE', errno: 29, description: 'Illegal seek' },
|
||||||
|
{ code: 'EROFS', errno: 30, description: 'Read-only file system' },
|
||||||
|
{ code: 'EMLINK', errno: 31, description: 'Too many links' },
|
||||||
|
{ code: 'EPIPE', errno: 32, description: 'Broken pipe' },
|
||||||
|
{ code: 'EDOM', errno: 33, description: 'Math argument out of domain of func' },
|
||||||
|
{ code: 'ERANGE', errno: 34, description: 'Math result not representable' }
|
||||||
|
]
|
||||||
|
|
||||||
|
export default {
|
||||||
|
lookup(errno: number): string | undefined {
|
||||||
|
return errs.find(err => err.errno === errno)?.description
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,44 +1,122 @@
|
|||||||
|
import errno from './errno'
|
||||||
|
import ffi from 'ffi'
|
||||||
import { deref } from 'ref'
|
import { deref } from 'ref'
|
||||||
import * as ref from 'ref'
|
import ref from 'ref'
|
||||||
import * as ffi from 'ffi'
|
import Struct from 'ref-struct'
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// FFI "Types"
|
||||||
|
|
||||||
const PgfDB = ref.types.int
|
const PgfDB = ref.types.int
|
||||||
const PgfDBPtr = ref.refType(PgfDB)
|
const PgfDBPtr = ref.refType(PgfDB)
|
||||||
const PgfRevision = ref.types.int
|
const PgfRevision = ref.types.int
|
||||||
const PgfRevisionPtr = ref.refType(PgfRevision)
|
const PgfRevisionPtr = ref.refType(PgfRevision)
|
||||||
const PgfExn = ref.types.int
|
const PgfExn = Struct({
|
||||||
const PgfExnPtr = ref.refType(PgfExn)
|
type: ref.types.int,
|
||||||
// const stringPtr = ref.refType(ref.types.CString)
|
code: ref.types.int,
|
||||||
|
msg: ref.types.CString
|
||||||
const PGF = ffi.Library('libpgf', {
|
|
||||||
'pgf_read_pgf': [ PgfDBPtr, [ 'string', PgfRevisionPtr, PgfExnPtr ] ],
|
|
||||||
'pgf_write_pgf': [ 'void', [ 'string', PgfDBPtr, PgfRevisionPtr, PgfExnPtr ] ],
|
|
||||||
})
|
})
|
||||||
|
const PgfExnPtr = ref.refType(PgfExn)
|
||||||
|
const PgfText = Struct({
|
||||||
|
size: ref.types.size_t,
|
||||||
|
text: ref.types.char // char[]
|
||||||
|
})
|
||||||
|
const PgfTextPtr = ref.refType(PgfText)
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// TypeScript Types
|
||||||
|
|
||||||
interface Pointer extends Buffer {
|
interface Pointer extends Buffer {
|
||||||
deref(): any
|
deref(): any
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
class PGFError extends Error {
|
||||||
db: Pointer,
|
constructor(message: string) {
|
||||||
revision: number
|
super(message)
|
||||||
}
|
|
||||||
const state: State = {
|
|
||||||
db: ref.NULL_POINTER as Pointer,
|
|
||||||
revision: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
readPGF: function (path: string) {
|
|
||||||
const rev = ref.alloc(PgfRevision) as Pointer
|
|
||||||
const err = ref.alloc(PgfExn) as Pointer
|
|
||||||
const db = PGF.pgf_read_pgf(path, rev, err)
|
|
||||||
if (err.deref() === 0) {
|
|
||||||
state.db = db
|
|
||||||
state.revision = rev.deref()
|
|
||||||
console.log(`ok`)
|
|
||||||
} else {
|
|
||||||
console.error(`error ${err.deref()}`)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// FFI
|
||||||
|
|
||||||
|
const runtime = ffi.Library('libpgf', {
|
||||||
|
'pgf_read_pgf': [ PgfDBPtr, [ ref.types.CString, PgfRevisionPtr, PgfExnPtr ] ],
|
||||||
|
'pgf_write_pgf': [ ref.types.void, [ ref.types.CString, PgfDBPtr, PgfRevisionPtr, PgfExnPtr ] ],
|
||||||
|
'pgf_abstract_name': [ PgfTextPtr, [ PgfDBPtr, PgfRevision, PgfExnPtr ] ],
|
||||||
|
})
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
|
||||||
|
function handleError (errPtr: Pointer): void {
|
||||||
|
const err = errPtr.deref()
|
||||||
|
switch (err.type) {
|
||||||
|
// PGF_EXN_NONE
|
||||||
|
case 0: return
|
||||||
|
|
||||||
|
// PGF_EXN_SYSTEM_ERROR
|
||||||
|
case 1:
|
||||||
|
const desc = errno.lookup(err.code)
|
||||||
|
throw new Error(`${desc}: ${err.msg}`)
|
||||||
|
|
||||||
|
// PGF_EXN_PGF_ERROR
|
||||||
|
case 2:
|
||||||
|
throw new PGFError(err.msg)
|
||||||
|
|
||||||
|
// PGF_EXN_OTHER_ERROR
|
||||||
|
case 3:
|
||||||
|
throw new Error(err.msg)
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`unknown error type: ${err.type}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function PgfText_AsString (txtPtr: Pointer) {
|
||||||
|
const txtSize = txtPtr.deref().size
|
||||||
|
const charPtr = ref.reinterpret(txtPtr, txtSize, ref.types.size_t.size)
|
||||||
|
return charPtr.toString('utf8')
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// PGF grammar object
|
||||||
|
|
||||||
|
class PGF {
|
||||||
|
db: Pointer
|
||||||
|
revision: number
|
||||||
|
|
||||||
|
constructor () {
|
||||||
|
this.db = ref.NULL_POINTER as Pointer
|
||||||
|
this.revision = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
getAbstractName () {
|
||||||
|
const err = ref.alloc(PgfExn) as Pointer
|
||||||
|
const txt = runtime.pgf_abstract_name(this.db, this.revision, err)
|
||||||
|
handleError(err)
|
||||||
|
return PgfText_AsString(txt)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// PGF module functions
|
||||||
|
|
||||||
|
function readPGF (path: string): PGF {
|
||||||
|
const rev = ref.alloc(PgfRevision) as Pointer
|
||||||
|
const err = ref.alloc(PgfExn) as Pointer
|
||||||
|
const db = runtime.pgf_read_pgf(path, rev, err)
|
||||||
|
handleError(err)
|
||||||
|
|
||||||
|
const state = new PGF()
|
||||||
|
state.db = db
|
||||||
|
state.revision = rev.deref()
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Exposed library API
|
||||||
|
|
||||||
|
export default {
|
||||||
|
readPGF
|
||||||
|
}
|
||||||
|
|||||||
5
src/runtime/javascript/jest.config.js
Normal file
5
src/runtime/javascript/jest.config.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
|
||||||
|
module.exports = {
|
||||||
|
preset: 'ts-jest',
|
||||||
|
testEnvironment: 'node'
|
||||||
|
};
|
||||||
3196
src/runtime/javascript/package-lock.json
generated
3196
src/runtime/javascript/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@
|
|||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npx tsc",
|
"build": "npx tsc",
|
||||||
"test": "ts-node ./tests/basic.ts"
|
"test": "npx jest"
|
||||||
},
|
},
|
||||||
"author": "John J. Camilleri",
|
"author": "John J. Camilleri",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
@@ -14,7 +14,9 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/ffi": "^0.2.3",
|
"@types/ffi": "^0.2.3",
|
||||||
"ts-node": "^10.2.1",
|
"@types/jest": "^27.0.2",
|
||||||
|
"jest": "^27.2.4",
|
||||||
|
"ts-jest": "^27.0.5",
|
||||||
"typescript": "^4.4.3"
|
"typescript": "^4.4.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
16
src/runtime/javascript/tests/basic.test.ts
Normal file
16
src/runtime/javascript/tests/basic.test.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import PGF from '../index'
|
||||||
|
|
||||||
|
test('readPGF successful', () => {
|
||||||
|
let gr = PGF.readPGF('../haskell/tests/basic.pgf')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('readPGF missing file', () => {
|
||||||
|
expect(() => {
|
||||||
|
PGF.readPGF('abc.pgf')
|
||||||
|
}).toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('abstract name', () => {
|
||||||
|
let gr = PGF.readPGF('../haskell/tests/basic.pgf')
|
||||||
|
expect(gr.getAbstractName()).toBe('basic')
|
||||||
|
})
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import PGF from '../index'
|
|
||||||
|
|
||||||
PGF.readPGF('../haskell/tests/basic.pgf')
|
|
||||||
Reference in New Issue
Block a user