• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Permify / permify / 19368484886

14 Nov 2025 03:00PM UTC coverage: 85.879% (+0.08%) from 85.795%
19368484886

push

github

web-flow
Merge pull request #2607 from Permify/feature/update-cli-commands

refactor(cmd/permify): streamline command registration in main function

332 of 367 new or added lines in 6 files covered. (90.46%)

7 existing lines in 3 files now uncovered.

9597 of 11175 relevant lines covered (85.88%)

290.87 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.0
/pkg/development/coverage/coverage.go
1
package coverage
2

3
import (
4
        "fmt"
5
        "slices"
6

7
        "github.com/Permify/permify/pkg/attribute"
8
        "github.com/Permify/permify/pkg/development/file"
9
        "github.com/Permify/permify/pkg/dsl/compiler"
10
        "github.com/Permify/permify/pkg/dsl/parser"
11
        base "github.com/Permify/permify/pkg/pb/base/v1"
12
        "github.com/Permify/permify/pkg/tuple"
13
)
14

15
// SchemaCoverageInfo represents the overall coverage information for a schema
16
type SchemaCoverageInfo struct {
17
        EntityCoverageInfo         []EntityCoverageInfo
18
        TotalRelationshipsCoverage int
19
        TotalAttributesCoverage    int
20
        TotalAssertionsCoverage    int
21
}
22

23
// EntityCoverageInfo represents coverage information for a single entity
24
type EntityCoverageInfo struct {
25
        EntityName string
26

27
        UncoveredRelationships       []string
28
        CoverageRelationshipsPercent int
29

30
        UncoveredAttributes       []string
31
        CoverageAttributesPercent int
32

33
        UncoveredAssertions       map[string][]string
34
        CoverageAssertionsPercent map[string]int
35
}
36

37
// SchemaCoverage represents the expected coverage for a schema entity
38
//
39
// Example schema:
40
//
41
//        entity user {}
42
//
43
//        entity organization {
44
//            relation admin @user
45
//            relation member @user
46
//        }
47
//
48
//        entity repository {
49
//            relation parent @organization
50
//            relation owner  @user @organization#admin
51
//            permission edit   = parent.admin or owner
52
//            permission delete = owner
53
//        }
54
//
55
// Expected relationships coverage:
56
//   - organization#admin@user
57
//   - organization#member@user
58
//   - repository#parent@organization
59
//   - repository#owner@user
60
//   - repository#owner@organization#admin
61
//
62
// Expected assertions coverage:
63
//   - repository#edit
64
//   - repository#delete
65
type SchemaCoverage struct {
66
        EntityName    string
67
        Relationships []string
68
        Attributes    []string
69
        Assertions    []string
70
}
71

72
// Run analyzes the coverage of relationships, attributes, and assertions
73
// for a given schema shape and returns the coverage information
74
func Run(shape file.Shape) SchemaCoverageInfo {
3✔
75
        definitions, err := parseAndCompileSchema(shape.Schema)
3✔
76
        if err != nil {
3✔
77
                return SchemaCoverageInfo{}
×
78
        }
×
79

80
        refs := extractSchemaReferences(definitions)
3✔
81
        entityCoverageInfos := calculateEntityCoverages(refs, shape)
3✔
82

3✔
83
        return buildSchemaCoverageInfo(entityCoverageInfos)
3✔
84
}
85

86
// parseAndCompileSchema parses and compiles the schema into entity definitions
87
func parseAndCompileSchema(schema string) ([]*base.EntityDefinition, error) {
3✔
88
        p, err := parser.NewParser(schema).Parse()
3✔
89
        if err != nil {
3✔
NEW
90
                return nil, err
×
NEW
91
        }
×
92

93
        definitions, _, err := compiler.NewCompiler(true, p).Compile()
3✔
94
        if err != nil {
3✔
NEW
95
                return nil, err
×
96
        }
×
97

98
        return definitions, nil
3✔
99
}
100

101
// extractSchemaReferences extracts all coverage references from entity definitions
102
func extractSchemaReferences(definitions []*base.EntityDefinition) []SchemaCoverage {
3✔
103
        refs := make([]SchemaCoverage, len(definitions))
3✔
104
        for idx, entityDef := range definitions {
19✔
105
                refs[idx] = extractEntityReferences(entityDef)
16✔
106
        }
16✔
107
        return refs
3✔
108
}
109

110
// extractEntityReferences extracts relationships, attributes, and assertions from an entity definition
111
func extractEntityReferences(entity *base.EntityDefinition) SchemaCoverage {
16✔
112
        coverage := SchemaCoverage{
16✔
113
                EntityName:    entity.GetName(),
16✔
114
                Relationships: extractRelationships(entity),
16✔
115
                Attributes:    extractAttributes(entity),
16✔
116
                Assertions:    extractAssertions(entity),
16✔
117
        }
16✔
118
        return coverage
16✔
119
}
16✔
120

121
// extractRelationships extracts all relationship references from an entity
122
func extractRelationships(entity *base.EntityDefinition) []string {
16✔
123
        relationships := []string{}
16✔
124

16✔
125
        for _, relation := range entity.GetRelations() {
43✔
126
                for _, reference := range relation.GetRelationReferences() {
66✔
127
                        formatted := formatRelationship(
39✔
128
                                entity.GetName(),
39✔
129
                                relation.GetName(),
39✔
130
                                reference.GetType(),
39✔
131
                                reference.GetRelation(),
39✔
132
                        )
39✔
133
                        relationships = append(relationships, formatted)
39✔
134
                }
39✔
135
        }
136

137
        return relationships
16✔
138
}
139

140
// extractAttributes extracts all attribute references from an entity
141
func extractAttributes(entity *base.EntityDefinition) []string {
16✔
142
        attributes := []string{}
16✔
143

16✔
144
        for _, attr := range entity.GetAttributes() {
16✔
NEW
145
                formatted := formatAttribute(entity.GetName(), attr.GetName())
×
NEW
146
                attributes = append(attributes, formatted)
×
NEW
147
        }
×
148

149
        return attributes
16✔
150
}
151

152
// extractAssertions extracts all permission/assertion references from an entity
153
func extractAssertions(entity *base.EntityDefinition) []string {
16✔
154
        assertions := []string{}
16✔
155

16✔
156
        for _, permission := range entity.GetPermissions() {
52✔
157
                formatted := formatAssertion(entity.GetName(), permission.GetName())
36✔
158
                assertions = append(assertions, formatted)
36✔
159
        }
36✔
160

161
        return assertions
16✔
162
}
163

164
// calculateEntityCoverages calculates coverage for all entities
165
func calculateEntityCoverages(refs []SchemaCoverage, shape file.Shape) []EntityCoverageInfo {
3✔
166
        entityCoverageInfos := []EntityCoverageInfo{}
3✔
167

3✔
168
        for _, ref := range refs {
19✔
169
                entityCoverageInfo := calculateEntityCoverage(ref, shape)
16✔
170
                entityCoverageInfos = append(entityCoverageInfos, entityCoverageInfo)
16✔
171
        }
16✔
172

173
        return entityCoverageInfos
3✔
174
}
175

176
// calculateEntityCoverage calculates coverage for a single entity
177
func calculateEntityCoverage(ref SchemaCoverage, shape file.Shape) EntityCoverageInfo {
16✔
178
        entityCoverageInfo := newEntityCoverageInfo(ref.EntityName)
16✔
179

16✔
180
        // Calculate relationships coverage
16✔
181
        entityCoverageInfo.UncoveredRelationships = findUncoveredRelationships(
16✔
182
                ref.EntityName,
16✔
183
                ref.Relationships,
16✔
184
                shape.Relationships,
16✔
185
        )
16✔
186
        entityCoverageInfo.CoverageRelationshipsPercent = calculateCoveragePercent(
16✔
187
                ref.Relationships,
16✔
188
                entityCoverageInfo.UncoveredRelationships,
16✔
189
        )
16✔
190

16✔
191
        // Calculate attributes coverage
16✔
192
        entityCoverageInfo.UncoveredAttributes = findUncoveredAttributes(
16✔
193
                ref.EntityName,
16✔
194
                ref.Attributes,
16✔
195
                shape.Attributes,
16✔
196
        )
16✔
197
        entityCoverageInfo.CoverageAttributesPercent = calculateCoveragePercent(
16✔
198
                ref.Attributes,
16✔
199
                entityCoverageInfo.UncoveredAttributes,
16✔
200
        )
16✔
201

16✔
202
        // Calculate assertions coverage for each scenario
16✔
203
        for _, scenario := range shape.Scenarios {
35✔
204
                uncovered := findUncoveredAssertions(
19✔
205
                        ref.EntityName,
19✔
206
                        ref.Assertions,
19✔
207
                        scenario.Checks,
19✔
208
                        scenario.EntityFilters,
19✔
209
                )
19✔
210
                // Only add to UncoveredAssertions if there are uncovered assertions
19✔
211
                if len(uncovered) > 0 {
28✔
212
                        entityCoverageInfo.UncoveredAssertions[scenario.Name] = uncovered
9✔
213
                }
9✔
214
                entityCoverageInfo.CoverageAssertionsPercent[scenario.Name] = calculateCoveragePercent(
19✔
215
                        ref.Assertions,
19✔
216
                        uncovered,
19✔
217
                )
19✔
218
        }
219

220
        return entityCoverageInfo
16✔
221
}
222

223
// newEntityCoverageInfo creates a new EntityCoverageInfo with initialized fields
224
func newEntityCoverageInfo(entityName string) EntityCoverageInfo {
16✔
225
        return EntityCoverageInfo{
16✔
226
                EntityName:                   entityName,
16✔
227
                UncoveredRelationships:       []string{},
16✔
228
                UncoveredAttributes:          []string{},
16✔
229
                CoverageAssertionsPercent:    make(map[string]int),
16✔
230
                UncoveredAssertions:          make(map[string][]string),
16✔
231
                CoverageRelationshipsPercent: 0,
16✔
232
                CoverageAttributesPercent:    0,
16✔
233
        }
16✔
234
}
16✔
235

236
// findUncoveredRelationships finds relationships that are not covered in the shape
237
func findUncoveredRelationships(entityName string, expected, actual []string) []string {
16✔
238
        covered := extractCoveredRelationships(entityName, actual)
16✔
239
        uncovered := []string{}
16✔
240

16✔
241
        for _, relationship := range expected {
55✔
242
                if !slices.Contains(covered, relationship) {
53✔
243
                        uncovered = append(uncovered, relationship)
14✔
244
                }
14✔
245
        }
246

247
        return uncovered
16✔
248
}
249

250
// findUncoveredAttributes finds attributes that are not covered in the shape
251
func findUncoveredAttributes(entityName string, expected, actual []string) []string {
16✔
252
        covered := extractCoveredAttributes(entityName, actual)
16✔
253
        uncovered := []string{}
16✔
254

16✔
255
        for _, attr := range expected {
16✔
NEW
256
                if !slices.Contains(covered, attr) {
×
NEW
257
                        uncovered = append(uncovered, attr)
×
NEW
258
                }
×
259
        }
260

261
        return uncovered
16✔
262
}
263

264
// findUncoveredAssertions finds assertions that are not covered in the shape
265
func findUncoveredAssertions(entityName string, expected []string, checks []file.Check, filters []file.EntityFilter) []string {
19✔
266
        covered := extractCoveredAssertions(entityName, checks, filters)
19✔
267
        uncovered := []string{}
19✔
268

19✔
269
        for _, assertion := range expected {
57✔
270
                if !slices.Contains(covered, assertion) {
69✔
271
                        uncovered = append(uncovered, assertion)
31✔
272
                }
31✔
273
        }
274

275
        return uncovered
19✔
276
}
277

278
// buildSchemaCoverageInfo builds the final SchemaCoverageInfo with total coverage
279
func buildSchemaCoverageInfo(entityCoverageInfos []EntityCoverageInfo) SchemaCoverageInfo {
3✔
280
        relationshipsCoverage, attributesCoverage, assertionsCoverage := calculateTotalCoverage(entityCoverageInfos)
3✔
281

3✔
282
        return SchemaCoverageInfo{
3✔
283
                EntityCoverageInfo:         entityCoverageInfos,
3✔
284
                TotalRelationshipsCoverage: relationshipsCoverage,
3✔
285
                TotalAttributesCoverage:    attributesCoverage,
3✔
286
                TotalAssertionsCoverage:    assertionsCoverage,
3✔
287
        }
3✔
288
}
3✔
289

290
// calculateCoveragePercent calculates coverage percentage based on total and uncovered elements
291
func calculateCoveragePercent(totalElements, uncoveredElements []string) int {
51✔
292
        totalCount := len(totalElements)
51✔
293
        if totalCount == 0 {
79✔
294
                return 100
28✔
295
        }
28✔
296

297
        coveredCount := totalCount - len(uncoveredElements)
23✔
298
        return (coveredCount * 100) / totalCount
23✔
299
}
300

301
// calculateTotalCoverage calculates average coverage percentages across all entities
302
func calculateTotalCoverage(entities []EntityCoverageInfo) (int, int, int) {
3✔
303
        var (
3✔
304
                totalRelationships        int
3✔
305
                totalCoveredRelationships int
3✔
306
                totalAttributes           int
3✔
307
                totalCoveredAttributes    int
3✔
308
                totalAssertions           int
3✔
309
                totalCoveredAssertions    int
3✔
310
        )
3✔
311

3✔
312
        for _, entity := range entities {
19✔
313
                totalRelationships++
16✔
314
                totalCoveredRelationships += entity.CoverageRelationshipsPercent
16✔
315

16✔
316
                totalAttributes++
16✔
317
                totalCoveredAttributes += entity.CoverageAttributesPercent
16✔
318

16✔
319
                for _, assertionPercent := range entity.CoverageAssertionsPercent {
35✔
320
                        totalAssertions++
19✔
321
                        totalCoveredAssertions += assertionPercent
19✔
322
                }
19✔
323
        }
324

325
        return calculateAverageCoverage(totalRelationships, totalCoveredRelationships),
3✔
326
                calculateAverageCoverage(totalAttributes, totalCoveredAttributes),
3✔
327
                calculateAverageCoverage(totalAssertions, totalCoveredAssertions)
3✔
328
}
329

330
// calculateAverageCoverage calculates average coverage with zero-division guard
331
func calculateAverageCoverage(total, covered int) int {
9✔
332
        if total == 0 {
9✔
NEW
333
                return 100
×
UNCOV
334
        }
×
335
        return covered / total
9✔
336
}
337

338
// extractCoveredRelationships extracts covered relationships for a given entity from the shape
339
func extractCoveredRelationships(entityName string, relationships []string) []string {
16✔
340
        covered := []string{}
16✔
341

16✔
342
        for _, relationship := range relationships {
322✔
343
                tup, err := tuple.Tuple(relationship)
306✔
344
                if err != nil {
306✔
345
                        continue
×
346
                }
347

348
                if tup.GetEntity().GetType() != entityName {
565✔
349
                        continue
259✔
350
                }
351

352
                formatted := formatRelationship(
47✔
353
                        tup.GetEntity().GetType(),
47✔
354
                        tup.GetRelation(),
47✔
355
                        tup.GetSubject().GetType(),
47✔
356
                        tup.GetSubject().GetRelation(),
47✔
357
                )
47✔
358
                covered = append(covered, formatted)
47✔
359
        }
360

361
        return covered
16✔
362
}
363

364
// extractCoveredAttributes extracts covered attributes for a given entity from the shape
365
func extractCoveredAttributes(entityName string, attributes []string) []string {
16✔
366
        covered := []string{}
16✔
367

16✔
368
        for _, attrStr := range attributes {
16✔
369
                a, err := attribute.Attribute(attrStr)
×
370
                if err != nil {
×
NEW
371
                        continue
×
372
                }
373

NEW
374
                if a.GetEntity().GetType() != entityName {
×
UNCOV
375
                        continue
×
376
                }
377

NEW
378
                formatted := formatAttribute(a.GetEntity().GetType(), a.GetAttribute())
×
NEW
379
                covered = append(covered, formatted)
×
380
        }
381

382
        return covered
16✔
383
}
384

385
// extractCoveredAssertions extracts covered assertions for a given entity from checks and filters
386
func extractCoveredAssertions(entityName string, checks []file.Check, filters []file.EntityFilter) []string {
19✔
387
        covered := []string{}
19✔
388

19✔
389
        // Extract from checks
19✔
390
        for _, check := range checks {
55✔
391
                entity, err := tuple.E(check.Entity)
36✔
392
                if err != nil {
36✔
NEW
393
                        continue
×
394
                }
395

396
                if entity.GetType() != entityName {
65✔
397
                        continue
29✔
398
                }
399

400
                for permission := range check.Assertions {
15✔
401
                        formatted := formatAssertion(entity.GetType(), permission)
8✔
402
                        covered = append(covered, formatted)
8✔
403
                }
8✔
404
        }
405

406
        // Extract from entity filters
407
        for _, filter := range filters {
26✔
408
                if filter.EntityType != entityName {
12✔
409
                        continue
5✔
410
                }
411

412
                for permission := range filter.Assertions {
4✔
413
                        formatted := formatAssertion(filter.EntityType, permission)
2✔
414
                        covered = append(covered, formatted)
2✔
415
                }
2✔
416
        }
417

418
        return covered
19✔
419
}
420

421
// formatRelationship formats a relationship string
422
func formatRelationship(entityName, relationName, subjectType, subjectRelation string) string {
86✔
423
        if subjectRelation != "" {
105✔
424
                return fmt.Sprintf("%s#%s@%s#%s", entityName, relationName, subjectType, subjectRelation)
19✔
425
        }
19✔
426
        return fmt.Sprintf("%s#%s@%s", entityName, relationName, subjectType)
67✔
427
}
428

429
// formatAttribute formats an attribute string
NEW
430
func formatAttribute(entityName, attributeName string) string {
×
NEW
431
        return fmt.Sprintf("%s#%s", entityName, attributeName)
×
NEW
432
}
×
433

434
// formatAssertion formats an assertion/permission string
435
func formatAssertion(entityName, permissionName string) string {
46✔
436
        return fmt.Sprintf("%s#%s", entityName, permissionName)
46✔
437
}
46✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc