1// Copyright 2023 The Bazel Authors. All rights reserved. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package python 16 17import ( 18 "context" 19 "testing" 20 21 "github.com/stretchr/testify/assert" 22) 23 24func TestParseImportStatements(t *testing.T) { 25 t.Parallel() 26 units := []struct { 27 name string 28 code string 29 filepath string 30 result []module 31 }{ 32 { 33 name: "not has import", 34 code: "a = 1\nb = 2", 35 filepath: "", 36 result: nil, 37 }, 38 { 39 name: "has import", 40 code: "import unittest\nimport os.path\nfrom foo.bar import abc.xyz", 41 filepath: "abc.py", 42 result: []module{ 43 { 44 Name: "unittest", 45 LineNumber: 1, 46 Filepath: "abc.py", 47 From: "", 48 }, 49 { 50 Name: "os.path", 51 LineNumber: 2, 52 Filepath: "abc.py", 53 From: "", 54 }, 55 { 56 Name: "foo.bar.abc.xyz", 57 LineNumber: 3, 58 Filepath: "abc.py", 59 From: "foo.bar", 60 }, 61 }, 62 }, 63 { 64 name: "has import in def", 65 code: `def foo(): 66 import unittest 67`, 68 filepath: "abc.py", 69 result: []module{ 70 { 71 Name: "unittest", 72 LineNumber: 2, 73 Filepath: "abc.py", 74 From: "", 75 }, 76 }, 77 }, 78 { 79 name: "invalid syntax", 80 code: "import os\nimport", 81 filepath: "abc.py", 82 result: []module{ 83 { 84 Name: "os", 85 LineNumber: 1, 86 Filepath: "abc.py", 87 From: "", 88 }, 89 }, 90 }, 91 { 92 name: "import as", 93 code: "import os as b\nfrom foo import bar as c# 123", 94 filepath: "abc.py", 95 result: []module{ 96 { 97 Name: "os", 98 LineNumber: 1, 99 Filepath: "abc.py", 100 From: "", 101 }, 102 { 103 Name: "foo.bar", 104 LineNumber: 2, 105 Filepath: "abc.py", 106 From: "foo", 107 }, 108 }, 109 }, 110 // align to https://docs.python.org/3/reference/simple_stmts.html#index-34 111 { 112 name: "complex import", 113 code: "from unittest import *\nfrom foo import (bar as c, baz, qux as d)\nfrom . import abc", 114 result: []module{ 115 { 116 Name: "unittest.*", 117 LineNumber: 1, 118 From: "unittest", 119 }, 120 { 121 Name: "foo.bar", 122 LineNumber: 2, 123 From: "foo", 124 }, 125 { 126 Name: "foo.baz", 127 LineNumber: 2, 128 From: "foo", 129 }, 130 { 131 Name: "foo.qux", 132 LineNumber: 2, 133 From: "foo", 134 }, 135 }, 136 }, 137 } 138 for _, u := range units { 139 t.Run(u.name, func(t *testing.T) { 140 p := NewFileParser() 141 code := []byte(u.code) 142 p.SetCodeAndFile(code, "", u.filepath) 143 output, err := p.Parse(context.Background()) 144 assert.NoError(t, err) 145 assert.Equal(t, u.result, output.Modules) 146 }) 147 } 148} 149 150func TestParseComments(t *testing.T) { 151 t.Parallel() 152 units := []struct { 153 name string 154 code string 155 result []comment 156 }{ 157 { 158 name: "not has comment", 159 code: "a = 1\nb = 2", 160 result: nil, 161 }, 162 { 163 name: "has comment", 164 code: "# a = 1\n# b = 2", 165 result: []comment{"# a = 1", "# b = 2"}, 166 }, 167 { 168 name: "has comment in if", 169 code: "if True:\n # a = 1\n # b = 2", 170 result: []comment{"# a = 1", "# b = 2"}, 171 }, 172 { 173 name: "has comment inline", 174 code: "import os# 123\nfrom pathlib import Path as b#456", 175 result: []comment{"# 123", "#456"}, 176 }, 177 } 178 for _, u := range units { 179 t.Run(u.name, func(t *testing.T) { 180 p := NewFileParser() 181 code := []byte(u.code) 182 p.SetCodeAndFile(code, "", "") 183 output, err := p.Parse(context.Background()) 184 assert.NoError(t, err) 185 assert.Equal(t, u.result, output.Comments) 186 }) 187 } 188} 189 190func TestParseMain(t *testing.T) { 191 t.Parallel() 192 units := []struct { 193 name string 194 code string 195 result bool 196 }{ 197 { 198 name: "not has main", 199 code: "a = 1\nb = 2", 200 result: false, 201 }, 202 { 203 name: "has main in function", 204 code: `def foo(): 205 if __name__ == "__main__": 206 a = 3 207`, 208 result: false, 209 }, 210 { 211 name: "has main", 212 code: ` 213import unittest 214 215from lib import main 216 217 218class ExampleTest(unittest.TestCase): 219 def test_main(self): 220 self.assertEqual( 221 "", 222 main([["A", 1], ["B", 2]]), 223 ) 224 225 226if __name__ == "__main__": 227 unittest.main() 228`, 229 result: true, 230 }, 231 } 232 for _, u := range units { 233 t.Run(u.name, func(t *testing.T) { 234 p := NewFileParser() 235 code := []byte(u.code) 236 p.SetCodeAndFile(code, "", "") 237 output, err := p.Parse(context.Background()) 238 assert.NoError(t, err) 239 assert.Equal(t, u.result, output.HasMain) 240 }) 241 } 242} 243 244func TestParseFull(t *testing.T) { 245 p := NewFileParser() 246 code := []byte(`from bar import abc`) 247 p.SetCodeAndFile(code, "foo", "a.py") 248 output, err := p.Parse(context.Background()) 249 assert.NoError(t, err) 250 assert.Equal(t, ParserOutput{ 251 Modules: []module{{Name: "bar.abc", LineNumber: 1, Filepath: "foo/a.py", From: "bar"}}, 252 Comments: nil, 253 HasMain: false, 254 FileName: "a.py", 255 }, *output) 256} 257