Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor ExercismGenerator to use Tonel file map #372

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
6 changes: 3 additions & 3 deletions dev/src/Exercise@TwoFer/TwoFerTest.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -75,17 +75,17 @@ TwoFerTest >> setUp [
]

{ #category : #tests }
TwoFerTest >> testANameGiven [
TwoFerTest >> test01_ANameGiven [
self assert: (twoFer who: 'Alice') equals: 'One for Alice, one for me.'
]

{ #category : #tests }
TwoFerTest >> testAnotherNameGiven [
TwoFerTest >> test02_AnotherNameGiven [
self assert: (twoFer who: 'Bob') equals: 'One for Bob, one for me.'
]

{ #category : #tests }
TwoFerTest >> testNoNameGiven [
TwoFerTest >> test03_NoNameGiven [
self assert: twoFer who equals: 'One for you, one for me.'

]
158 changes: 78 additions & 80 deletions dev/src/ExercismDev/ExercismExerciseGenerator.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@ Class {
#superclass : #Object,
#instVars : [
'numberGenerated',
'testCounter'
],
#classVars : [
'DefaultPath'
'testCounter',
'problems'
],
#category : #'ExercismDev-TestGenerator'
}
Expand All @@ -24,28 +22,21 @@ ExercismExerciseGenerator class >> convertLegacyTagsToPackages [
do: [ :t | t promoteAsExercismRPackage ]
]

{ #category : #examples }
ExercismExerciseGenerator class >> defaultPath [
^ DefaultPath ifNil: [ self defaultPath: FileLocator home pathString]
]

{ #category : #examples }
ExercismExerciseGenerator class >> defaultPath: pathString [
^ DefaultPath := pathString
]

{ #category : #examples }
ExercismExerciseGenerator class >> generate [
"This is the entry point for generating exercism compatible source files that can be checked into
the exercism/pharo project. e.g. self generate"

<example>
| path |
path := UIManager default
chooseDirectory: 'Select the /exercises location in a full Exercism/problem-specifications git project'
path: self defaultPath.

path ifNotNil: [ self new generateFrom: (self defaultPath: path) ]
self new generateFrom: ExercismProblemSpecification all "or allNeedingGeneration ?? ask user ??"
]

{ #category : #examples }
ExercismExerciseGenerator class >> problemSpecificationsFrom: filePathReference [
^ filePathReference entries
collect: [ :entry | ExercismProblemSpecification newFromFileRef: entry reference ].

]

{ #category : #examples }
Expand Down Expand Up @@ -173,10 +164,10 @@ ExercismExerciseGenerator >> generateCodeShouldRaiseOn: output variable: variabl
]

{ #category : #generation }
ExercismExerciseGenerator >> generateExerciseFrom: aFileSystemReference [
| testName testDescription testSpecification testJson testClass testRoot testVariable testMetaData versionString |
ExercismExerciseGenerator >> generateExerciseFrom: spec [
| testMetaData testClass |


" ExercismProblemSpecification newFromFileRef: aFileSystemReference.
testRoot := '' join: ((aFileSystemReference basename splitOn: $-) collect: [ :w | w capitalized ]).
testName := testRoot, 'Test'.
testVariable := (testRoot, 'Calculator') asValidSelector asString.
Expand All @@ -193,19 +184,23 @@ ExercismExerciseGenerator >> generateExerciseFrom: aFileSystemReference [

testJson := STONJSON fromString: testSpecification.
versionString := testJson at: 'version'.

testMetaData := (WriteStream on: '') nextPutAll: testMetaData;
nextPutAll: ('exercise: "{1}"' format: {testRoot}); cr;
nextPutAll: ('version: "{1}"' format: {versionString}); cr; contents.

testClass := self generateTestClass: testName tag: testRoot variable: testVariable.
"
Transcript crShow: 'Generating ', spec classNameForTest.
spec hasCanonicalData ifFalse: [ ^self ].
testClass := self generateTestClassUsing: spec.
"self generateTestVariableAccessors: testVariable in: testClass."
self generateSetupOn: testClass using: testVariable assigning: testRoot.
self generateTestMethodsOn: testClass calling: testVariable using: testJson prefix: ''.
self generateMetaDataFor: testClass description: testDescription version: versionString metaData: testMetaData.
self generateSetupOn: testClass using: spec.
self halt. "Work in progress. Step through next method to validate."
self generateTestMethodsOn: testClass using: spec.

testMetaData := ''. "spec metaDataString".
testMetaData := (WriteStream on: '') nextPutAll: testMetaData;
nextPutAll: ('exercise: "{1}"' format: {spec mixedCaseName}); cr;
nextPutAll: ('version: "{1}"' format: {spec version}); cr; contents.
self generateMetaDataFor: testClass description: spec description version: spec version metaData: testMetaData.

self numberGenerated: self numberGenerated + 1.
self log: 'successfully created' for: testName
self log: 'successfully created' for: spec classNameForTest



Expand All @@ -214,16 +209,14 @@ ExercismExerciseGenerator >> generateExerciseFrom: aFileSystemReference [
]

{ #category : #generation }
ExercismExerciseGenerator >> generateFrom: filePathReference [
ExercismExerciseGenerator >> generateFrom: problemSpecifications [
(RPackageOrganizer default
includesPackageNamed: self defaultPackageName)
ifFalse: [ RPackageOrganizer default createPackageNamed: self defaultPackageName ].

self crLog: 'Generating new TestCases from specification: ', filePathReference printString.

self numberGenerated: 0.
filePathReference entries
do: [ :entry | self generateExerciseFrom: entry reference ].
(problemSpecifications select: #hasCanonicalData)
do: [ :spec | self generateExerciseFrom: spec ].

self
crLog: ('Generation complete. Created {1} Tests!'
Expand Down Expand Up @@ -266,65 +259,70 @@ ExercismExerciseGenerator >> generateMetaDataFor: testClass description: testDes
]

{ #category : #generation }
ExercismExerciseGenerator >> generateSetupOn: testClass using: testVariable assigning: testRoot [
| output src |
ExercismExerciseGenerator >> generateSetupOn: testClass using: spec [
| methodSource |

output := (WriteStream on: '').
output nextPutAll: 'setUp'; cr.
output tab; nextPutAll: 'super setUp.'; cr.
output tab; nextPutAll: testVariable, ' := '; nextPutAll: testRoot, ' new'.
methodSource := (WriteStream on: '').
methodSource
nextPutAll: 'setUp'; cr;
tab; nextPutAll: 'super setUp.'; cr;
tab; nextPutAll: spec testVariable, ' := '; nextPutAll: spec classNameForSolution, ' new'.

src := output contents.
self compile: src for: testClass selector: #setUp protocol: 'running'
self
compile: methodSource contents
for: testClass
selector: #setUp
protocol: 'running'

]

{ #category : #generation }
ExercismExerciseGenerator >> generateTestClass: testName tag: tagName variable: testVariable [
ExercismExerciseGenerator >> generateTestClassUsing: spec [

self resetTestCounter.

^ExercismTest
subclass: testName asSymbol
instanceVariableNames: testVariable
subclass: spec classNameForTest asSymbol
instanceVariableNames: spec testVariable
classVariableNames: ''
poolDictionaries: ''
package: 'ExercismWIP-', tagName
package: 'ExercismWIP-', spec mixedCaseName

]

{ #category : #generation }
ExercismExerciseGenerator >> generateTestMethodsOn: testClass calling: testVariable using: testJson prefix: aPrefixString [
| instance methodName parameters testResult testPrefix methodNameSegment |

(testJson at: 'cases')
do: [ :case |
methodNameSegment := (((testJson at: 'cases') size > 1 ) or: [ aPrefixString notEmpty ])
ifTrue: [ (case at: 'description') asCamelCase asValidKeyword capitalized ]
ifFalse: [ '' ].
methodName := aPrefixString
ifEmpty: [ methodNameSegment withoutPrefix: 'And' ]
ifNotEmpty: [ aPrefixString, methodNameSegment ].

(case includesKey: 'cases')
ifTrue: [ self
generateTestMethodsOn: testClass
calling: testVariable
using: case
prefix: methodName ]
ifFalse: [
instance := case at: 'property'.
parameters := case at: 'input'.
testResult := case at: 'expected'.

testPrefix := 'test{1}_' format: {self nextTestCounter asTwoCharacterString}.
self
generateTestNamed: (testPrefix, methodName) asSymbol
in: testClass
variable: testVariable
selector: instance
parameters: parameters
expecting: testResult ] ]
ExercismExerciseGenerator >> generateTestMethodsOn: testClass calling: testVariable using: spec prefix: aPrefixString [
"Processes... https://github.com/exercism/problem-specifications/blob/master/canonical-schema.json"
| groups instance methodName parameters testResult testPrefix methodNameSegment |

groups := (spec tests collect: [ :test | test groupCount ]) max.
spec tests
do: [ :test |
testPrefix := 'test{1}_' format: {self nextTestCounter asTwoCharacterString}.
self
generateTestNamed: (testPrefix, test methodNameSegment) asSymbol
in: testClass
variable: spec testVariable
selector: test property
parameters: test input
expecting: test expected ]
]

{ #category : #generation }
ExercismExerciseGenerator >> generateTestMethodsOn: testClass using: spec [
"Processes... https://github.com/exercism/problem-specifications/blob/master/canonical-schema.json"
| testPrefix |

spec tests
do: [ :test |
testPrefix := 'test{1}_' format: {self nextTestCounter asTwoCharacterString}.
self
generateTestNamed: (testPrefix, test methodNameSegment) asSymbol
in: testClass
variable: spec testVariable
selector: test property
parameters: test input
expecting: test expected ]
]

{ #category : #generation }
Expand Down
109 changes: 83 additions & 26 deletions dev/src/ExercismDev/ExercismGenerator.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,19 @@ Class {
#name : #ExercismGenerator,
#superclass : #Object,
#classVars : [
'DefaultPath'
'LastPath',
'UseConfigletGenerate'
],
#category : #'ExercismDev-Generator'
}

{ #category : #helper }
ExercismGenerator class >> defaultPath [
^ DefaultPath ifNil: [ self defaultPath: FileLocator home pathString]
]
{ #category : #'as yet unclassified' }
ExercismGenerator class >> defaultGenerationPath [
|exercismRepo|
exercismRepo := LGitRepository allInstances detect: [ :repo |
repo workingDirectory pathSegments last = 'pharo-smalltalk'].
^ exercismRepo ifNotNil: [(exercismRepo workingDirectory / 'exercises') pathString ]

{ #category : #helper }
ExercismGenerator class >> defaultPath: pathString [
^ DefaultPath := pathString
]

{ #category : #generation }
Expand All @@ -34,12 +34,30 @@ ExercismGenerator class >> generate [
the exercism/pharo project. e.g. self generate"

<example>
| path |
path := UIManager default
chooseDirectory: 'Select the /exercises location in a fresh Exercism/Pharo git project'
path: self defaultPath.
| generationPath defaultGenerationPath |
defaultGenerationPath := self defaultGenerationPath.
generationPath := defaultGenerationPath ifNotNil: [
(self confirm: 'Generate to default location?' , String cr, defaultGenerationPath)
ifTrue: [ defaultGenerationPath ]
].

generationPath ifNil: [ generationPath :=
UIManager default
chooseDirectory: 'Select the "pharo-smalltalk/exercises" folder to generate into'
path: self lastPath.
].

path ifNotNil: [ self new generateTo: (self defaultPath: path) ]
generationPath ifNotNil: [ self new generateTo: (self lastPath: generationPath) asFileReference ]
]

{ #category : #helper }
ExercismGenerator class >> lastPath [
^ LastPath ifNil: [ self defaultPath: FileLocator home pathString]
]

{ #category : #helper }
ExercismGenerator class >> lastPath: pathString [
^ LastPath := pathString
]

{ #category : #helper }
Expand Down Expand Up @@ -80,6 +98,50 @@ ExercismGenerator >> generateCustomDataFor: anExercismExercise to: destinationDi
lf ]]
]

{ #category : #'as yet unclassified' }
ExercismGenerator >> generateDefinitions: definitions to: aFileReference [
| tonelFileMap |
tonelFileMap := ExTonelWriter new mappedSnapshot: (MCSnapshot fromDefinitions: definitions).
tonelFileMap
keysAndValuesDo: [ :file :stream |
(aFileReference / file) ensureCreateFile
writeStreamDo: [ :fileStream | fileStream nextPutAll: stream contents ] ].
]

{ #category : #helper }
ExercismGenerator >> generateExercise: exercise to: exerciseRootDirRef [
"Generate the Tonel source files for a package. Answer the exercise directory reference"

| exerciseName exerciseDirRef metaDirRef solutionDirRef exerciseDefs solutionDefs |

"The following directory structure is expected by Exercism infrastructure.
Canonical reference is output of their utlitity command `configlet generate` "
exerciseName := ExercismExercise exerciseNameFrom: exercise exercisePackage.
exerciseDirRef := exerciseRootDirRef / exerciseName.
metaDirRef := exerciseRootDirRef / exerciseName / '.meta'.
solutionDirRef := exerciseRootDirRef / exerciseName / '.meta' / 'solution'.

exerciseDirRef ensureCreateDirectory.
exerciseDirRef deleteAll.

exerciseDefs := exercise definitions select: #isExercise.
self generateDefinitions: exerciseDefs to: exerciseDirRef.

solutionDefs := exercise definitions select: #isSolution.
self generateDefinitions: solutionDefs to: solutionDirRef.

self generateReadmeFor: exercise to: exerciseDirRef.
self generateReadmeHintFor: exercise to: metaDirRef.

exercise isCustom ifTrue: [ self generateCustomDataFor: exercise to: metaDirRef ]
]

{ #category : #helper }
ExercismGenerator >> generateReadmeFor: exercise to: destinationDirectory [
(destinationDirectory / 'README.md') ensureCreateFile
writeStreamDo: [ :stream | stream nextPutAll: exercise readme withUnixLineEndings ]
]

{ #category : #helper }
ExercismGenerator >> generateReadmeHintFor: anExercismExercise to: destinationDirectory [
"Generate markdown hints, that exercism configlet will pickup for readme.md files
Expand Down Expand Up @@ -138,19 +200,14 @@ ExercismGenerator >> generateSourceFilesFor: packageOrTag to: filePathString [
]

{ #category : #generation }
ExercismGenerator >> generateTo: filePathReference [
| cmd result basePathReference |
ExercismGenerator >> generateTo: exerciseRootDirectoryReference [
| repoFileReference |

ExercismExercise allExercises select: [:ex | ex isActive ] thenDo: [:ex |
self generateSourceFilesFor: ex exercisePackage to: filePathReference ].
ExercismExercise allExercises
select: [:exercise | exercise isActive ]
thenDo: [:exercise | self generateExercise: exercise to: exerciseRootDirectoryReference ].

basePathReference := filePathReference parent.
ExercismConfigGenerator generateTo: basePathReference.

cmd := 'configlet generate ', (basePathReference pathString surroundedBySingleQuotes).
result := PipeableOSProcess waitForCommand: cmd.

result succeeded
ifFalse: [
self error: 'failure running "configlet generate" - ' , result outputAndError printString ]
repoFileReference := exerciseRootDirectoryReference parent.
ExercismConfigGenerator generateTo: repoFileReference.

]
Loading